| @@ -541,22 +541,19 @@ export function renderFailuresJson(tree, opts = {}) { | ||
| export function renderLessonsMarkdown(tree, opts = {}) { | ||
| const analysis = analyzeTree(tree); | ||
| - | const lines = ['# TreeTrace Lessons', '']; | |
| + | const lines = ['# Lessons', '']; | |
| if (!analysis.lessons.length) { | ||
| lines.push('No high-confidence failure lessons were detected in this session.'); | ||
| lines.push(''); | ||
| return lines.join('\n'); | ||
| } | ||
| - | analysis.lessons.forEach((lesson, i) => { | |
| - | lines.push(`## ${i + 1}. ${escapeMd(lesson.title)}`); | |
| - | lines.push(''); | |
| - | lines.push(escapeMd(lesson.text)); | |
| - | lines.push(''); | |
| + | analysis.lessons.forEach((lesson) => { | |
| const ids = lesson.nodeIds; | ||
| const shown = ids.slice(0, 8).join(', '); | ||
| - | lines.push(`Source nodes: ${shown}${ids.length > 8 ? ` (+${ids.length - 8} more)` : ''}`); | |
| - | lines.push(''); | |
| + | const overflow = ids.length > 8 ? `, +${ids.length - 8} more` : ''; | |
| + | lines.push(`- **${escapeMd(lesson.title)}.** ${escapeMd(compactLessonText(lesson.text))} [${shown}${overflow}]`); | |
| }); | ||
| + | lines.push(''); | |
| return lines.join('\n'); | ||
| } | ||
| @@ -570,56 +567,49 @@ export function renderMemoryMarkdown(tree, opts = {}) { | ||
| const projectName = opts.projectName || 'this project'; | ||
| const nodes = tree.nodes || []; | ||
| const live = (n) => n.status !== 'abandoned'; | ||
| - | const lines = [`# TreeTrace Agent Memory`, '', `Project: ${escapeMd(projectName)}`, '']; | |
| + | const lines = [`Project: ${escapeMd(projectName)}`, '']; | |
| - | lines.push('## Constraints the user enforced'); | |
| - | lines.push(''); | |
| const constraints = extractConstraints(nodes); | ||
| if (constraints.length) { | ||
| + | lines.push('## Constraints'); | |
| for (const label of constraints) lines.push(`- ${escapeMd(truncate(label, 140))}`); | ||
| - | } else { | |
| - | lines.push('- No explicit constraints were flagged. Follow the accepted decisions in the handoff brief.'); | |
| + | lines.push(''); | |
| } | ||
| - | lines.push(''); | |
| - | lines.push('## Lessons from this lineage'); | |
| - | lines.push(''); | |
| if (analysis.lessons.length) { | ||
| - | for (const lesson of analysis.lessons.slice(0, 8)) lines.push(`- ${escapeMd(lesson.text)}`); | |
| - | } else { | |
| - | lines.push('- No high-confidence failure lessons were detected yet.'); | |
| + | lines.push('## Lessons'); | |
| + | for (const lesson of analysis.lessons.slice(0, 8)) { | |
| + | const ids = lesson.nodeIds || []; | |
| + | const shown = ids.slice(0, 8).join(', '); | |
| + | const overflow = ids.length > 8 ? `, +${ids.length - 8} more` : ''; | |
| + | const nodeIds = shown ? ` [${shown}${overflow}]` : ''; | |
| + | lines.push(`- ${escapeMd(lesson.title)}: ${escapeMd(compactLessonText(lesson.text))}${nodeIds}`); | |
| + | } | |
| + | lines.push(''); | |
| } | ||
| - | lines.push(''); | |
| - | lines.push('## Known bad paths'); | |
| - | lines.push(''); | |
| const badPaths = analysis.failures.filter((f) => f.type === 'abandoned_path').slice(0, 6); | ||
| if (badPaths.length) { | ||
| + | lines.push('## Bad paths'); | |
| for (const failure of badPaths) lines.push(`- ${escapeMd(failure.summary)}`); | ||
| - | } else { | |
| - | lines.push('- No abandoned paths were detected in this session.'); | |
| + | lines.push(''); | |
| } | ||
| - | lines.push(''); | |
| - | lines.push('## Security-sensitive actions'); | |
| - | lines.push(''); | |
| const security = analysis.failures | ||
| .filter((f) => f.type === 'security_or_privacy_risk') | ||
| .sort((a, b) => tierRank(b.tier) - tierRank(a.tier)) | ||
| .slice(0, 8); | ||
| if (security.length) { | ||
| - | lines.push('Treat these as durable warnings; re-verify before touching the same surfaces:'); | |
| + | lines.push('## Security'); | |
| for (const f of security) { | ||
| const tag = f.tier === 'inferred' ? 'stated intent' : f.tier; | ||
| - | lines.push(`- (${tag}) ${escapeMd(truncate(f.evidence, 200))}`); | |
| + | const nodeId = f.firstSeenNodeId ? ` [${f.firstSeenNodeId}]` : ''; | |
| + | lines.push(`- (${tag})${nodeId} ${escapeMd(truncate(compactEvidenceText(f.evidence), 200))}`); | |
| } | ||
| - | } else { | |
| - | lines.push('- No security-sensitive actions or intents were detected in this session.'); | |
| + | lines.push(''); | |
| } | ||
| - | lines.push(''); | |
| - | lines.push('## Preferred next work'); | |
| - | lines.push(''); | |
| + | lines.push('## Next'); | |
| const strategic = nodes.filter( | ||
| (n) => | ||
| live(n) && | ||
| @@ -628,19 +618,31 @@ export function renderMemoryMarkdown(tree, opts = {}) { | ||
| ); | ||
| const latest = latestByTime(strategic); | ||
| if (latest) { | ||
| - | lines.push(`- Continue the most recent accepted direction: ${escapeMd(truncate(latest.title, 140))}`); | |
| + | lines.push(`- Continue: ${escapeMd(truncate(latest.title, 140))}`); | |
| } else { | ||
| lines.push(`- No open forward direction was stated; resume the goal of ${escapeMd(projectName)} and confirm scope with the user.`); | ||
| } | ||
| const openCorrections = nodes | ||
| .filter((n) => live(n) && n.kind === 'correction' && isStrategicDirection(n)) | ||
| .slice(-3); | ||
| - | for (const n of openCorrections) lines.push(`- Keep this correction satisfied: ${escapeMd(truncate(n.title, 120))}`); | |
| + | for (const n of openCorrections) lines.push(`- Constraint: ${escapeMd(truncate(n.title, 120))}`); | |
| lines.push(''); | ||
| return lines.join('\n'); | ||
| } | ||
| + | function compactLessonText(text) { | |
| + | const normalized = String(text || '').replace(/\s+/g, ' ').trim(); | |
| + | const evidenceAt = normalized.indexOf('Specifically:'); | |
| + | return evidenceAt === -1 ? normalized : normalized.slice(evidenceAt + 'Specifically:'.length).trim(); | |
| + | } | |
| + | ||
| + | function compactEvidenceText(text) { | |
| + | const normalized = String(text || '').replace(/\s+/g, ' ').trim(); | |
| + | const quoted = normalized.match(/"[^"]+"/); | |
| + | return quoted ? quoted[0] : normalized; | |
| + | } | |
| + | ||
| export function latestByTime(nodes) { | ||
| if (!nodes || !nodes.length) return null; | ||
| const timed = nodes.filter((n) => tsOf(n) !== null); |
| @@ -13,10 +13,7 @@ export function renderHandoff(tree, opts = {}) { | ||
| const lastAccepted = latestByTime(accepted); | ||
| lines.push(`# Handoff brief: ${escapeMdTags(projectName)}`); | ||
| - | lines.push(''); | |
| - | lines.push( | |
| - | `You are taking over an AI-assisted project. This brief was distilled from the real prompt lineage (${stats.promptCount} prompts, ${stats.sessionCount} sessions). Read it fully before acting.` | |
| - | ); | |
| + | lines.push(`${stats.promptCount} ${plural(stats.promptCount, 'prompt')} · ${stats.sessionCount} ${plural(stats.sessionCount, 'session')}`); | |
| lines.push(''); | ||
| if (root) { | ||
| @@ -30,9 +27,9 @@ export function renderHandoff(tree, opts = {}) { | ||
| lines.push(''); | ||
| if (lastCheckpoint) { | ||
| lines.push(`Last checkpoint: ${escapeMdTags(lastCheckpoint.text.trim())}`); | ||
| + | if (lastAccepted && lastAccepted !== lastCheckpoint) lines.push(''); | |
| } | ||
| if (lastAccepted && lastAccepted !== lastCheckpoint) { | ||
| - | lines.push(''); | |
| lines.push(`Most recent accepted direction: ${escapeMdTags(lastAccepted.text.trim())}`); | ||
| } | ||
| lines.push(''); | ||
| @@ -41,7 +38,7 @@ export function renderHandoff(tree, opts = {}) { | ||
| (n) => (n.kind === 'direction' || n.kind === 'scope-change') && isStrategicDirection(n) | ||
| ); | ||
| if (decisions.length) { | ||
| - | lines.push('## Accepted decisions (in order)'); | |
| + | lines.push('## Accepted decisions'); | |
| lines.push(''); | ||
| decisions.forEach((n, i) => lines.push(`${i + 1}. ${escapeMdTags(truncate(n.text.replace(/\s+/g, ' '), 360))}`)); | ||
| lines.push(''); | ||
| @@ -49,9 +46,7 @@ export function renderHandoff(tree, opts = {}) { | ||
| const corrections = accepted.filter((n) => n.kind === 'correction'); | ||
| if (corrections.length) { | ||
| - | lines.push('## Constraints learned the hard way'); | |
| - | lines.push(''); | |
| - | lines.push('These corrections were issued during the build. Do not repeat the mistakes they fixed:'); | |
| + | lines.push('## Constraints'); | |
| lines.push(''); | ||
| corrections.forEach((n) => lines.push(`- ${escapeMdTags(truncate(n.text.replace(/\s+/g, ' '), 300))}`)); | ||
| lines.push(''); | ||
| @@ -61,29 +56,30 @@ export function renderHandoff(tree, opts = {}) { | ||
| (n) => n.status === 'abandoned' && (!n.parent || n.parent.status !== 'abandoned') | ||
| ); | ||
| if (abandoned.length) { | ||
| - | lines.push('## Known dead ends'); | |
| - | lines.push(''); | |
| - | lines.push('These approaches were tried and abandoned. Avoid unless told otherwise:'); | |
| + | lines.push('## Dead ends'); | |
| lines.push(''); | ||
| abandoned.forEach((n) => lines.push(`- ${escapeMdTags(truncate(n.text.replace(/\s+/g, ' '), 300))}`)); | ||
| lines.push(''); | ||
| } | ||
| if (analysis.lessons.length) { | ||
| - | lines.push('## Agent memory lessons'); | |
| + | lines.push('## Lessons'); | |
| lines.push(''); | ||
| analysis.lessons.slice(0, 6).forEach((lesson) => { | ||
| - | lines.push(`- ${escapeMdTags(truncate(lesson.text.replace(/\s+/g, ' '), 320))}`); | |
| + | lines.push(`- ${escapeMdTags(lesson.title)}: ${escapeMdTags(truncate(compactLessonText(lesson.text), 320))}`); | |
| }); | ||
| lines.push(''); | ||
| } | ||
| - | lines.push('## First task'); | |
| - | lines.push(''); | |
| - | lines.push( | |
| - | 'Confirm you understand the goal, the accepted decisions, and the constraints above, then ask the user what to tackle next (or continue the most recent accepted direction if instructed to proceed autonomously).' | |
| - | ); | |
| - | lines.push(''); | |
| - | ||
| return lines.join('\n'); | ||
| } | ||
| + | ||
| + | function plural(count, singular) { | |
| + | return count === 1 ? singular : `${singular}s`; | |
| + | } | |
| + | ||
| + | function compactLessonText(text) { | |
| + | const normalized = String(text || '').replace(/\s+/g, ' ').trim(); | |
| + | const evidenceAt = normalized.indexOf('Specifically:'); | |
| + | return evidenceAt === -1 ? normalized : normalized.slice(evidenceAt + 'Specifically:'.length).trim(); | |
| + | } |
| @@ -17,13 +17,9 @@ export function renderMarkdown(tree, opts = {}) { | ||
| const { stats, roots, nodes, sessions } = tree; | ||
| const lines = []; | ||
| - | lines.push(`# 🌳 Prompt Tree: ${escapeMd(projectName)}`); | |
| + | lines.push(`# Prompt Tree: ${escapeMd(projectName)}`); | |
| lines.push(''); | ||
| lines.push(`> ${banner(stats)}`); | ||
| - | lines.push('>'); | |
| - | lines.push( | |
| - | `> The prompt lineage that built this project, extracted from real sessions, curated and redacted by the author, generated by [treetrace](${REPO_URL}).` | |
| - | ); | |
| lines.push(''); | ||
| const root = nodes.find((n) => n.kind === 'root') || nodes[0]; | ||
| @@ -70,37 +66,8 @@ export function renderMarkdown(tree, opts = {}) { | ||
| lines.push(''); | ||
| } | ||
| - | const corrections = nodes.filter((n) => n.kind === 'correction'); | |
| - | const abandoned = nodes.filter( | |
| - | (n) => n.status === 'abandoned' && (!n.parent || n.parent.status !== 'abandoned') | |
| - | ); | |
| - | if (corrections.length || abandoned.length) { | |
| - | lines.push('## Course corrections & dead ends'); | |
| - | lines.push(''); | |
| - | if (abandoned.length) { | |
| - | lines.push(`**${plural(abandoned.length, 'abandoned branch', 'abandoned branches')}:**`); | |
| - | lines.push(''); | |
| - | for (const n of abandoned) { | |
| - | lines.push(`- ✗ ${escapeMd(truncate(n.title, 110))}`); | |
| - | } | |
| - | lines.push(''); | |
| - | } | |
| - | if (corrections.length) { | |
| - | lines.push(`**${plural(corrections.length, 'correction')} along the way:**`); | |
| - | lines.push(''); | |
| - | for (const n of corrections) { | |
| - | lines.push(`- ↩ ${escapeMd(truncate(n.title, 110))}`); | |
| - | } | |
| - | lines.push(''); | |
| - | } | |
| - | } | |
| - | ||
| lines.push('## Reusable Prompt Pack'); | ||
| lines.push(''); | ||
| - | lines.push( | |
| - | 'A distilled, replayable version of the accepted path. Paste into a fresh agent to rebuild something like this:' | |
| - | ); | |
| - | lines.push(''); | |
| const pack = promptPack(nodes); | ||
| const fence = '`'.repeat(Math.max(3, longestRun(pack, '`') + 1)); | ||
| lines.push(`${fence}text`); | ||
| @@ -110,12 +77,7 @@ export function renderMarkdown(tree, opts = {}) { | ||
| lines.push('---'); | ||
| lines.push(''); | ||
| - | lines.push( | |
| - | `*Generated by [treetrace](${REPO_URL})${version ? ` · v${version}` : ''} · ${plural(stats.promptCount, 'prompt')} across ${plural( | |
| - | stats.sessionCount, | |
| - | 'session' | |
| - | )} · machine-readable lineage in \`.treetrace/tree.json\` ([schema](${REPO_URL}/blob/main/SCHEMA.md))*` | |
| - | ); | |
| + | lines.push(`*[treetrace](${REPO_URL})${version ? ` v${version}` : ''} · [schema](${REPO_URL}/blob/main/SCHEMA.md)*`); | |
| lines.push(''); | ||
| return lines.join('\n'); |
| @@ -1,6 +1,4 @@ | ||
| - | import { analyzeTree, renderLessonsMarkdown, renderMemoryMarkdown, latestByTime } from './analyze.js'; | |
| - | import { renderHandoff } from './handoff.js'; | |
| - | import { renderMarkdown } from './render-md.js'; | |
| + | import { analyzeTree, latestByTime } from './analyze.js'; | |
| import { plural, truncate, escapeMd } from './util.js'; | ||
| import { REPO_URL } from './config.js'; | ||
| @@ -14,39 +12,29 @@ export function renderReportMarkdown(tree, opts = {}) { | ||
| lines.push(''); | ||
| lines.push(`Generated: ${generatedAt}`); | ||
| lines.push(''); | ||
| - | lines.push( | |
| - | 'This is the human-readable rollup. Keep the split `.treetrace/` artifacts for agents, CI, eval harnesses, and other tools.' | |
| - | ); | |
| - | lines.push(''); | |
| - | ||
| - | lines.push('## Read order'); | |
| - | lines.push(''); | |
| - | lines.push('1. `TREETRACE_REPORT.md` - human rollup and terminal-friendly report.'); | |
| - | lines.push('2. `PROMPT_TREE.md` - detailed prompt lineage and reusable prompt pack.'); | |
| - | lines.push('3. `.treetrace/lessons.md` - reusable correction memory.'); | |
| - | lines.push('4. `.treetrace/agent-memory.md` - compact memory for the next coding agent.'); | |
| - | lines.push('5. `.treetrace/tree.json`, `failures.json`, and `evals.jsonl` - machine-readable data.'); | |
| - | lines.push(''); | |
| lines.push('## Session summary'); | ||
| lines.push(''); | ||
| const { promptCount, rawPromptCount } = tree.stats; | ||
| const foldedTurns = (rawPromptCount || promptCount) - promptCount; | ||
| - | lines.push( | |
| - | foldedTurns > 0 | |
| - | ? `- Prompts: ${promptCount} (merged from ${rawPromptCount} raw turns; ${foldedTurns} continuation or duplicate turn${foldedTurns === 1 ? '' : 's'} folded in)` | |
| - | : `- Prompts: ${promptCount}` | |
| - | ); | |
| - | lines.push(`- Sessions: ${tree.stats.sessionCount}`); | |
| - | if (tree.stats.days) lines.push(`- Active span: ${plural(tree.stats.days, 'day')}`); | |
| - | if (tree.stats.corrections) lines.push(`- Corrections: ${tree.stats.corrections}`); | |
| - | if (tree.stats.abandonedBranches) lines.push(`- Abandoned branches: ${tree.stats.abandonedBranches}`); | |
| - | if (tree.stats.toolUses) lines.push(`- Tool calls: ${tree.stats.toolUses.toLocaleString()}`); | |
| - | if (tree.stats.filesTouched) lines.push(`- Files touched: ${tree.stats.filesTouched}`); | |
| const tc = analysis.summary.tierCounts || { verified: 0, high: 0, confirmed: 0, inferred: 0 }; | ||
| + | const promptPart = | |
| + | foldedTurns > 0 | |
| + | ? `Prompts: ${promptCount} (from ${rawPromptCount} raw turns)` | |
| + | : `Prompts: ${promptCount}`; | |
| + | const sessionParts = [ | |
| + | promptPart, | |
| + | `Sessions: ${tree.stats.sessionCount}`, | |
| + | tree.stats.days ? `Span: ${plural(tree.stats.days, 'day')}` : null, | |
| + | tree.stats.toolUses ? `Tool calls: ${tree.stats.toolUses.toLocaleString()}` : null, | |
| + | tree.stats.filesTouched ? `Files touched: ${tree.stats.filesTouched}` : null, | |
| + | ].filter(Boolean); | |
| + | lines.push(`- ${sessionParts.join(' ')}`); | |
| lines.push( | ||
| `- Failure signals: ${analysis.summary.totalFailureSignals} (verified ${tc.verified}, high ${tc.high || 0}, confirmed ${tc.confirmed}, inferred ${tc.inferred})` | ||
| ); | ||
| + | if (tree.stats.corrections) lines.push(`- Corrections: ${tree.stats.corrections}`); | |
| + | if (tree.stats.abandonedBranches) lines.push(`- Abandoned branches: ${tree.stats.abandonedBranches}`); | |
| if (analysis.summary.models && analysis.summary.models.length) { | ||
| lines.push(`- Models seen: ${analysis.summary.models.join(', ')}`); | ||
| } | ||
| @@ -59,16 +47,16 @@ export function renderReportMarkdown(tree, opts = {}) { | ||
| lines.push('## Output map'); | ||
| lines.push(''); | ||
| - | lines.push('| File | Use it for |'); | |
| - | lines.push('|------|------------|'); | |
| - | lines.push('| `TREETRACE_REPORT.md` | Human review, terminal output, quick context. |'); | |
| - | lines.push('| `PROMPT_TREE.md` | Full lineage narrative and replayable prompt pack. |'); | |
| - | lines.push('| `.treetrace/tree.json` | Canonical schema for tools and integrations. |'); | |
| - | lines.push('| `.treetrace/failures.json` | Failure labels, evidence, correction chains. |'); | |
| - | lines.push('| `.treetrace/hallucinations.json` | Referenced files, paths, imports, or packages that do not exist in the working tree. |'); | |
| - | lines.push('| `.treetrace/lessons.md` | Human-readable lessons. |'); | |
| - | lines.push('| `.treetrace/evals.jsonl` | Eval/regression cases; not meant to be pretty. |'); | |
| - | lines.push('| `.treetrace/agent-memory.md` | Short memory pack for Codex, Claude Code, Cursor, or another agent. |'); | |
| + | lines.push('| File | Purpose |'); | |
| + | lines.push('|------|---------|'); | |
| + | lines.push('| `TREETRACE_REPORT.md` | this file |'); | |
| + | lines.push('| `PROMPT_TREE.md` | prompt lineage + replay pack |'); | |
| + | lines.push('| `.treetrace/tree.json` | canonical schema |'); | |
| + | lines.push('| `.treetrace/failures.json` | labels + correction chains |'); | |
| + | lines.push('| `.treetrace/hallucinations.json` | unresolved references |'); | |
| + | lines.push('| `.treetrace/lessons.md` | correction memory |'); | |
| + | lines.push('| `.treetrace/evals.jsonl` | regression eval cases |'); | |
| + | lines.push('| `.treetrace/agent-memory.md` | next-agent memory pack |'); | |
| lines.push(''); | ||
| if (analysis.failures.length) { | ||
| @@ -80,7 +68,9 @@ export function renderReportMarkdown(tree, opts = {}) { | ||
| lines.push(''); | ||
| for (const failure of analysis.failures.slice(0, 8)) { | ||
| const meta = [failure.tier, confidencePct(failure.confidence), failure.model].filter(Boolean).join(', '); | ||
| - | lines.push(`- ${failure.id} (${failure.type}, ${meta}): ${escapeMd(failure.summary)}`); | |
| + | const nodeId = failure.firstSeenNodeId ? ` [${failure.firstSeenNodeId}]` : ''; | |
| + | const evidence = failure.evidence ? ` Evidence: ${escapeMd(truncate(failure.evidence, 180))}` : ''; | |
| + | lines.push(`- ${failure.id}${nodeId} (${failure.type}, ${meta}): ${escapeMd(failure.summary)}${evidence}`); | |
| } | ||
| if (analysis.failures.length > 8) { | ||
| lines.push(`- ... ${analysis.failures.length - 8} more in .treetrace/failures.json`); | ||
| @@ -94,37 +84,19 @@ export function renderReportMarkdown(tree, opts = {}) { | ||
| securityTrail.sort((a, b) => (rank[b.tier] || 0) - (rank[a.tier] || 0)); | ||
| lines.push('## Security audit trail'); | ||
| lines.push(''); | ||
| - | lines.push('Every time an agent touched auth, secrets, or access control in this session:'); | |
| - | lines.push(''); | |
| for (const f of securityTrail.slice(0, 12)) { | ||
| const tag = f.tier === 'inferred' ? 'stated intent' : f.tier; | ||
| - | lines.push(`- (${tag}) ${escapeMd(f.evidence)}${f.model ? ` (${f.model})` : ''}`); | |
| + | const nodeId = f.firstSeenNodeId ? ` [${f.firstSeenNodeId}]` : ''; | |
| + | lines.push(`- (${tag})${nodeId} ${escapeMd(f.evidence)}${f.model ? ` (${f.model})` : ''}`); | |
| } | ||
| lines.push(''); | ||
| } | ||
| - | lines.push('## Handoff brief'); | |
| - | lines.push(''); | |
| - | lines.push(demoteHeadings(stripTitle(renderHandoff(tree, opts)), 2)); | |
| - | lines.push(''); | |
| - | ||
| - | lines.push('## Agent memory'); | |
| - | lines.push(''); | |
| - | lines.push(demoteHeadings(stripTitle(renderMemoryMarkdown(tree, opts)), 2)); | |
| - | lines.push(''); | |
| - | ||
| - | lines.push('## Lessons'); | |
| - | lines.push(''); | |
| - | lines.push(demoteHeadings(stripTitle(renderLessonsMarkdown(tree, opts)), 2)); | |
| - | lines.push(''); | |
| - | ||
| - | lines.push('## Prompt tree'); | |
| - | lines.push(''); | |
| - | lines.push(demoteHeadings(stripTitle(renderMarkdown(tree, { ...opts, titlesOnly: opts.titlesOnly })), 2)); | |
| + | lines.push('## Artifacts'); | |
| lines.push(''); | ||
| + | lines.push('See: `PROMPT_TREE.md` · `.treetrace/lessons.md` · `.treetrace/agent-memory.md` · handoff: run `treetrace --handoff`'); | |
| lines.push('---'); | ||
| - | lines.push(''); | |
| lines.push(`Generated by [treetrace](${REPO_URL})${opts.version ? ` v${opts.version}` : ''}.`); | ||
| lines.push(''); | ||
| @@ -165,14 +137,6 @@ export function renderTerminalSummary(tree, opts = {}) { | ||
| return lines.join('\n'); | ||
| } | ||
| - | function stripTitle(markdown) { | |
| - | return markdown.replace(/^# .*(?:\r?\n){1,2}/, '').trim(); | |
| - | } | |
| - | ||
| - | function demoteHeadings(markdown, levels) { | |
| - | return markdown.replace(/^(#{1,5}) /gm, (m, hashes) => `${hashes}${'#'.repeat(levels)} `); | |
| - | } | |
| - | ||
| function confidencePct(confidence) { | ||
| return `${Math.round(confidence * 100)}%`; | ||
| } |
| @@ -104,107 +104,95 @@ export function renderSecurityReport(tree, projectDir, opts = {}) { | ||
| lines.push(''); | ||
| lines.push(`Generated: ${generatedAt}`); | ||
| lines.push(''); | ||
| - | lines.push( | |
| - | 'This report leads with concrete failure classes from the session. It reuses the same signals as the full TreeTrace analysis; it does not run a separate scanner.' | |
| - | ); | |
| - | lines.push(''); | |
| const anySignal = | ||
| f.surfaces.size || f.testSkips.length || f.riskyCommands.length || f.securitySignals.length || f.hallucinationResult.hallucinations.length; | ||
| if (!anySignal) { | ||
| - | lines.push('No security-sensitive touches, test changes, risky commands, hallucinated references, or stated security intents were detected in this session.'); | |
| + | lines.push('None detected.'); | |
| lines.push(''); | ||
| footer(lines, opts); | ||
| return lines.join('\n'); | ||
| } | ||
| - | lines.push('## 1. Did the agent touch security-sensitive surfaces?'); | |
| + | lines.push('## Surfaces touched'); | |
| lines.push(''); | ||
| if (f.surfaces.size) { | ||
| - | lines.push('Yes. Touched surfaces, with the files involved:'); | |
| - | lines.push(''); | |
| for (const surface of SURFACE_ORDER) { | ||
| const touches = f.surfaces.get(surface); | ||
| if (!touches || !touches.length) continue; | ||
| const files = [...new Set(touches.map((t) => t.file))].slice(0, 8); | ||
| - | lines.push(`- ${SURFACE_LABELS[surface]}: ${files.map((x) => `\`${escapeMd(truncate(x, 100))}\``).join(', ')}`); | |
| + | const nodeIds = [...new Set(touches.map((t) => t.nodeId).filter(Boolean))].slice(0, 8); | |
| + | const ids = nodeIds.length ? ` [${nodeIds.join(', ')}]` : ''; | |
| + | lines.push(`- ${SURFACE_LABELS[surface]}: ${files.map((x) => `\`${escapeMd(truncate(x, 100))}\``).join(', ')}${ids}`); | |
| } | ||
| } else { | ||
| - | lines.push('No edits to auth, secrets, access control, crypto, dependency config, CI, deployment, or test files were observed in the captured actions.'); | |
| + | lines.push('None detected.'); | |
| } | ||
| if (f.securitySignals.length) { | ||
| lines.push(''); | ||
| - | lines.push('Security signals from the analysis pass (highest tier first):'); | |
| + | lines.push('## Security signals (highest tier first)'); | |
| lines.push(''); | ||
| for (const s of f.securitySignals.slice(0, 12)) { | ||
| const tag = s.tier === 'inferred' ? 'stated intent' : s.tier; | ||
| - | lines.push(`- (${tag}) ${escapeMd(truncate(s.evidence, EVIDENCE_CAP))}${s.model ? ` (${s.model})` : ''}`); | |
| + | const nodeId = s.firstSeenNodeId ? ` [${s.firstSeenNodeId}]` : ''; | |
| + | lines.push(`- (${tag})${nodeId} ${escapeMd(truncate(s.evidence, EVIDENCE_CAP))}${s.model ? ` (${s.model})` : ''}`); | |
| } | ||
| } | ||
| lines.push(''); | ||
| - | lines.push('## 2. Did the agent disable or skip tests?'); | |
| + | lines.push('## Test skips'); | |
| lines.push(''); | ||
| if (f.testSkips.length) { | ||
| - | lines.push('Possible test removal or skipping was detected. Verify before trusting the suite:'); | |
| - | lines.push(''); | |
| for (const t of f.testSkips.slice(0, 8)) lines.push(`- (${t.nodeId}) ${escapeMd(t.evidence)}`); | ||
| } else { | ||
| - | lines.push('No evidence of disabled or skipped tests was found in prompts or captured actions.'); | |
| + | lines.push('None detected.'); | |
| } | ||
| lines.push(''); | ||
| - | lines.push('## 3. Did the agent run risky shell commands?'); | |
| + | lines.push('## Risky shell commands'); | |
| lines.push(''); | ||
| if (f.riskyCommands.length) { | ||
| - | lines.push('Yes. The following commands matched the risky-command patterns:'); | |
| - | lines.push(''); | |
| for (const r of f.riskyCommands.slice(0, 8)) lines.push(`- (${r.nodeId}) \`${escapeMd(r.command)}\`${r.model ? ` (${r.model})` : ''}`); | ||
| } else { | ||
| - | lines.push('No commands matched the risky-shell patterns (force pushes without review, recursive deletes, piped remote shells, world-writable chmod, destructive SQL).'); | |
| + | lines.push('None detected.'); | |
| } | ||
| lines.push(''); | ||
| - | lines.push('## 4. Did the agent reference files, paths, imports, or packages that do not exist?'); | |
| + | lines.push('## Hallucinated references'); | |
| lines.push(''); | ||
| if (!f.hallucinationResult.verifiedAgainstWorkingTree) { | ||
| - | lines.push('Not checked: no readable working tree was available for verification.'); | |
| + | lines.push('Working tree not available for verification.'); | |
| } else if (f.hallucinationResult.hallucinations.length) { | ||
| - | lines.push('Yes. The following references could not be verified against the working tree or declared dependencies:'); | |
| - | lines.push(''); | |
| for (const h of f.hallucinationResult.hallucinations.slice(0, 12)) { | ||
| - | lines.push(`- (${h.category}) ${escapeMd(truncate(h.evidence, EVIDENCE_CAP))}`); | |
| + | const nodeId = h.nodeId ? ` [${h.nodeId}]` : ''; | |
| + | lines.push(`- (${h.category})${nodeId} ${escapeMd(truncate(h.evidence, EVIDENCE_CAP))}`); | |
| } | ||
| - | lines.push(''); | |
| - | lines.push('File and path existence and import and package declaration are checked deterministically. Per-symbol or per-API resolution inside a module is not attempted.'); | |
| } else { | ||
| - | lines.push('No hallucinated files, paths, imports, or packages were detected. File and path existence and import and package declaration were checked against the working tree and manifests.'); | |
| + | lines.push('None detected.'); | |
| } | ||
| lines.push(''); | ||
| - | lines.push('## 5. What human correction should become a future eval or memory item?'); | |
| + | lines.push('## Corrections to promote'); | |
| lines.push(''); | ||
| const securityChains = f.analysis.correctionChains.filter((c) => c.failureType === 'security_or_privacy_risk'); | ||
| if (securityChains.length || f.corrections.length) { | ||
| - | lines.push('Turn these corrections into regression evals so the next agent inherits the constraint:'); | |
| - | lines.push(''); | |
| const seen = new Set(); | ||
| for (const chain of securityChains.slice(0, 6)) { | ||
| const corr = tree.nodes.find((n) => n.id === chain.correctionNodeId); | ||
| if (corr && !seen.has(corr.id)) { | ||
| seen.add(corr.id); | ||
| - | lines.push(`- (security correction) ${escapeMd(truncate(corr.text.replace(/\s+/g, ' '), 300))}`); | |
| + | lines.push(`- (${corr.id}) ${escapeMd(truncate(corr.text.replace(/\s+/g, ' '), 300))}`); | |
| } | ||
| } | ||
| for (const corr of f.corrections.slice(-6)) { | ||
| if (seen.has(corr.id)) continue; | ||
| seen.add(corr.id); | ||
| - | lines.push(`- ${escapeMd(truncate(corr.text.replace(/\s+/g, ' '), 300))}`); | |
| + | lines.push(`- (${corr.id}) ${escapeMd(truncate(corr.text.replace(/\s+/g, ' '), 300))}`); | |
| } | ||
| lines.push(''); | ||
| - | lines.push('Eval candidates from the analysis pass live in `.treetrace/evals.jsonl`; hallucination eval candidates live in `.treetrace/hallucinations.json`.'); | |
| + | lines.push('→ Eval candidates: .treetrace/evals.jsonl · .treetrace/hallucinations.json'); | |
| } else { | ||
| - | lines.push('No human correction was linked to a security-sensitive action in this session. If a security touch above was intentional, capture the rationale so the next agent does not flag it again.'); | |
| + | lines.push('None. If a security touch above was intentional, log the rationale.'); | |
| } | ||
| lines.push(''); |