Zion Boggan zionboggan.com ↗

Wire analysis into CLI, add combined report and Action, regenerate example artifacts

6191ce3   Zion Boggan committed on Jun 12, 2026 (1 week ago)
action.yml +14 -5
@@ -1,5 +1,5 @@
-name: 'treetrace'
-description: 'Generate or refresh PROMPT_TREE.md - the prompt lineage of this repository - from committed treetrace lineage data.'
+name: 'TreeTrace'
+description: 'Generate local prompt-lineage, human report, failure-analysis, eval, memory, and PROMPT_TREE.md artifacts.'
author: 'Zion Boggan'
branding:
icon: 'git-branch'
@@ -34,7 +34,7 @@ runs:
node -e "
const { readFileSync, writeFileSync } = require('fs');
const data = JSON.parse(readFileSync('.treetrace/tree.json','utf8'));
- console.log('lineage present:', data.nodes.length, 'nodes - PROMPT_TREE.md should be committed alongside it');
+ console.log('lineage present:', data.nodes.length, 'nodes - TREETRACE_REPORT.md and PROMPT_TREE.md should be committed alongside it');
"
else
echo "::warning::No source transcript or .treetrace/tree.json found - nothing to do."
@@ -46,9 +46,18 @@ runs:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
- if [ -f PROMPT_TREE.md ]; then
+ if [ -f TREETRACE_REPORT.md ]; then
{
- echo "### 🌳 Prompt tree"
+ echo "### TreeTrace report"
+ echo ""
+ head -c 4000 TREETRACE_REPORT.md
+ echo ""
+ echo "_Full report: TREETRACE_REPORT.md_"
+ } > /tmp/tt-comment.md
+ gh pr comment "${{ github.event.pull_request.number }}" --body-file /tmp/tt-comment.md
+ elif [ -f PROMPT_TREE.md ]; then
+ {
+ echo "### Prompt tree"
echo ""
head -c 4000 PROMPT_TREE.md
echo ""
examples/weather-dashboard/.treetrace/agent-memory.md +25 -0
@@ -0,0 +1,25 @@
+# TreeTrace Agent Memory
+
+Project: weather-dashboard
+
+## Durable project constraints
+
+- Keep the project local-first and privacy-first.
+- Treat the structured outputs as the core product.
+- Keep the human-readable report as one artifact among several.
+
+
+## Lessons from this lineage
+
+- Future agents should prefer the smallest implementation that satisfies the corrected product direction. Evidence: "No, scrap the radar map, it is too heavy. Keep the page lightweight, just the forecast cards."
+
+## Known bad paths
+
+
+- Do not narrow the project to only a README generator.
+
+## Preferred next work
+
+- Improve the failure-signal heuristics with real fixtures.
+- Add a compare mode for baseline and candidate exports.
+
examples/weather-dashboard/.treetrace/evals.jsonl +1 -0
@@ -0,0 +1 @@
+{"id":"eval_001","source":"treetrace","type":"scope_drift_detection","task":"Continue development while preserving the corrected direction from the session lineage.","context":"The work appears to have overbuilt the requested shape near \"Try using leaflet for an interactive radar map layer on top of the forecast.\"; corrected by \"No, scrap the radar map, it is too heavy.\".","input":"Continue development of the project while preserving the corrected direction and constraints.","expected_behavior":["Use the corrected prompt lineage as durable context","Do not repeat the documented failure mode"],"failure_mode":"Agent repeats overbuilt solution despite prior correction.","sourceNodeIds":["node_002","node_003","node_004"]}
examples/weather-dashboard/.treetrace/failures.json +43 -0
@@ -0,0 +1,43 @@
+{
+ "schemaVersion": "0.2",
+ "project": {
+ "name": "weather-dashboard",
+ "generatedAt": "2026-06-12T02:37:21.277Z"
+ },
+ "summary": {
+ "totalFailureSignals": 1,
+ "topFailureTypes": [
+ {
+ "type": "overbuilt_solution",
+ "count": 1
+ }
+ ],
+ "correctionChains": 1,
+ "evalCandidates": 1,
+ "lessons": 1
+ },
+ "failures": [
+ {
+ "id": "failure_001",
+ "type": "overbuilt_solution",
+ "confidence": 0.78,
+ "firstSeenNodeId": "node_002",
+ "correctedByNodeId": "node_003",
+ "summary": "The work appears to have overbuilt the requested shape near \"Try using leaflet for an interactive radar map layer on top of the forecast.\"; corrected by \"No, scrap the radar map, it is too heavy.\".",
+ "evidence": "User said: \"No, scrap the radar map, it is too heavy. Keep the page lightweight, just the forecast cards.\"",
+ "lesson": "Future agents should prefer the smallest implementation that satisfies the corrected product direction. Evidence: \"No, scrap the radar map, it is too heavy. Keep the page lightweight, just the forecast cards.\"",
+ "evalCandidate": true
+ }
+ ],
+ "correctionChains": [
+ {
+ "id": "chain_001",
+ "failureNodeId": "node_002",
+ "correctionNodeId": "node_003",
+ "resolvedNodeId": "node_004",
+ "failureType": "overbuilt_solution",
+ "confidence": "medium",
+ "summary": "The work appears to have overbuilt the requested shape near \"Try using leaflet for an interactive radar map layer on top of the forecast.\"; corrected by \"No, scrap the radar map, it is too heavy.\"."
+ }
+ ]
+}
examples/weather-dashboard/.treetrace/lessons.md +7 -0
@@ -0,0 +1,7 @@
+# TreeTrace Lessons
+
+## 1. Avoid overbuilding beyond the requested shape
+
+Future agents should prefer the smallest implementation that satisfies the corrected product direction. Evidence: "No, scrap the radar map, it is too heavy. Keep the page lightweight, just the forecast cards."
+
+Source nodes: node_002, node_003, node_004
examples/weather-dashboard/.treetrace/tree.json +192 -0
@@ -0,0 +1,192 @@
+{
+ "schemaVersion": "0.2",
+ "generator": {
+ "name": "treetrace",
+ "version": "0.2.0",
+ "url": "https://github.com/REPLACE-ME-ORG/treetrace"
+ },
+ "project": {
+ "name": "weather-dashboard",
+ "generatedAt": "2026-06-12T02:37:21.277Z",
+ "sourceType": "claude-code-jsonl"
+ },
+ "stats": {
+ "prompts": 4,
+ "sessions": 1,
+ "days": 1,
+ "corrections": 1,
+ "scopeChanges": 1,
+ "checkpoints": 0,
+ "abandonedBranches": 0,
+ "toolUses": 2,
+ "filesTouched": 1,
+ "models": [
+ "assistant-model"
+ ],
+ "firstTs": "2026-06-01T10:00:00.000Z",
+ "lastTs": "2026-06-01T10:12:00.000Z"
+ },
+ "analysis": {
+ "failureSignals": 1,
+ "correctionChains": 1,
+ "evalCandidates": 1,
+ "lessons": 1
+ },
+ "sessions": [
+ {
+ "id": "synthetic-session",
+ "title": "Build a weather dashboard",
+ "firstTs": "2026-06-01T10:00:00.000Z",
+ "lastTs": "2026-06-01T10:12:00.000Z",
+ "promptCount": 5,
+ "isContinuation": false
+ }
+ ],
+ "nodes": [
+ {
+ "id": "node_001",
+ "parentId": null,
+ "role": "user",
+ "kind": "root",
+ "title": "Build a weather dashboard web app that shows the forecast for Memphis using the NWS API.",
+ "text": "Build a weather dashboard web app that shows the forecast for Memphis using the NWS API. Keep it a single static page.",
+ "status": "accepted",
+ "nudges": 1,
+ "reruns": 0,
+ "session": "synthetic-session",
+ "timestamp": "2026-06-01T10:00:00.000Z",
+ "failureSignals": [],
+ "evalCandidate": false,
+ "lessonIds": [],
+ "sourceEventIds": [
+ "u1"
+ ]
+ },
+ {
+ "id": "node_002",
+ "parentId": "node_001",
+ "role": "user",
+ "kind": "direction",
+ "title": "Try using leaflet for an interactive radar map layer on top of the forecast.",
+ "text": "Try using leaflet for an interactive radar map layer on top of the forecast.",
+ "status": "accepted",
+ "nudges": 0,
+ "reruns": 0,
+ "session": "synthetic-session",
+ "timestamp": "2026-06-01T10:04:00.000Z",
+ "failureSignals": [
+ {
+ "type": "overbuilt_solution",
+ "confidence": 0.78,
+ "evidence": "User said: \"No, scrap the radar map, it is too heavy. Keep the page lightweight, just the forecast cards.\"",
+ "resolvedBy": "node_003"
+ }
+ ],
+ "evalCandidate": true,
+ "lessonIds": [
+ "lesson_001"
+ ],
+ "sourceEventIds": [
+ "u5"
+ ]
+ },
+ {
+ "id": "node_003",
+ "parentId": "node_002",
+ "role": "user",
+ "kind": "correction",
+ "title": "No, scrap the radar map, it is too heavy.",
+ "text": "No, scrap the radar map, it is too heavy. Keep the page lightweight, just the forecast cards.",
+ "status": "accepted",
+ "nudges": 0,
+ "reruns": 0,
+ "session": "synthetic-session",
+ "timestamp": "2026-06-01T10:09:00.000Z",
+ "failureSignals": [],
+ "evalCandidate": false,
+ "lessonIds": [],
+ "sourceEventIds": [
+ "u7"
+ ]
+ },
+ {
+ "id": "node_004",
+ "parentId": "node_003",
+ "role": "user",
+ "kind": "scope-change",
+ "title": "Actually wait - also add a settings panel so the user can switch cities.",
+ "text": "Actually wait - also add a settings panel so the user can switch cities. My test key is [REDACTED:anthropic-key] and the server is at [REDACTED:url-basic-auth]",
+ "status": "accepted",
+ "nudges": 0,
+ "reruns": 0,
+ "session": "synthetic-session",
+ "timestamp": "2026-06-01T10:12:00.000Z",
+ "failureSignals": [],
+ "evalCandidate": false,
+ "lessonIds": [],
+ "sourceEventIds": [
+ "u9"
+ ]
+ }
+ ],
+ "edges": [
+ {
+ "from": "node_001",
+ "to": "node_002",
+ "relationship": "refines"
+ },
+ {
+ "from": "node_002",
+ "to": "node_003",
+ "relationship": "corrects"
+ },
+ {
+ "from": "node_003",
+ "to": "node_004",
+ "relationship": "expands"
+ }
+ ],
+ "correctionChains": [
+ {
+ "id": "chain_001",
+ "failureNodeId": "node_002",
+ "correctionNodeId": "node_003",
+ "resolvedNodeId": "node_004",
+ "failureType": "overbuilt_solution",
+ "confidence": "medium",
+ "summary": "The work appears to have overbuilt the requested shape near \"Try using leaflet for an interactive radar map layer on top of the forecast.\"; corrected by \"No, scrap the radar map, it is too heavy.\"."
+ }
+ ],
+ "lessons": [
+ {
+ "id": "lesson_001",
+ "title": "Avoid overbuilding beyond the requested shape",
+ "nodeIds": [
+ "node_002",
+ "node_003",
+ "node_004"
+ ],
+ "text": "Future agents should prefer the smallest implementation that satisfies the corrected product direction. Evidence: \"No, scrap the radar map, it is too heavy. Keep the page lightweight, just the forecast cards.\""
+ }
+ ],
+ "evalCandidates": [
+ {
+ "id": "eval_001",
+ "source": "treetrace",
+ "type": "scope_drift_detection",
+ "task": "Continue development while preserving the corrected direction from the session lineage.",
+ "context": "The work appears to have overbuilt the requested shape near \"Try using leaflet for an interactive radar map layer on top of the forecast.\"; corrected by \"No, scrap the radar map, it is too heavy.\".",
+ "input": "Continue development of the project while preserving the corrected direction and constraints.",
+ "expected_behavior": [
+ "Use the corrected prompt lineage as durable context",
+ "Do not repeat the documented failure mode"
+ ],
+ "failure_mode": "Agent repeats overbuilt solution despite prior correction.",
+ "sourceNodeIds": [
+ "node_002",
+ "node_003",
+ "node_004"
+ ]
+ }
+ ]
+}
examples/weather-dashboard/TREETRACE_REPORT.md +165 -0
@@ -0,0 +1,165 @@
+# TreeTrace Report - weather-dashboard
+
+Generated: 2026-06-12T02:37:21.277Z
+
+This is the human-readable rollup. Keep the split `.treetrace/` artifacts for agents, CI, eval harnesses, and other tools.
+
+## Read order
+
+1. `TREETRACE_REPORT.md` - human rollup and terminal-friendly report.
+2. `PROMPT_TREE.md` - detailed prompt lineage and reusable prompt pack.
+3. `.treetrace/lessons.md` - reusable correction memory.
+4. `.treetrace/agent-memory.md` - compact memory for the next coding agent.
+5. `.treetrace/tree.json`, `failures.json`, and `evals.jsonl` - machine-readable data.
+
+## Session summary
+
+- Prompts: 4
+- Sessions: 1
+- Active span: 1 day
+- Corrections: 1
+- Tool calls: 2
+- Files touched: 1
+- Failure signals: 1
+- Eval candidates: 1
+- Lessons: 1
+
+## Output map
+
+| File | Use it for |
+|------|------------|
+| `TREETRACE_REPORT.md` | Human review, terminal output, quick context. |
+| `PROMPT_TREE.md` | Full lineage narrative and replayable prompt pack. |
+| `.treetrace/tree.json` | Canonical schema for tools and integrations. |
+| `.treetrace/failures.json` | Failure labels, evidence, correction chains. |
+| `.treetrace/lessons.md` | Human-readable lessons. |
+| `.treetrace/evals.jsonl` | Eval/regression cases; not meant to be pretty. |
+| `.treetrace/agent-memory.md` | Short memory pack for Codex, Claude Code, Cursor, or another agent. |
+
+## Failure signals
+
+- overbuilt_solution: 1
+
+- failure_001 (overbuilt_solution, 78%): The work appears to have overbuilt the requested shape near "Try using leaflet for an interactive radar map layer on top of the forecast."; corrected by "No, scrap the radar map, it is too heavy.".
+
+## Handoff brief
+
+You are taking over an AI-assisted project. This brief was distilled from the real prompt lineage (4 prompts, 1 sessions). Read it fully before acting.
+
+#### Original goal
+
+Build a weather dashboard web app that shows the forecast for Memphis using the NWS API. Keep it a single static page.
+
+#### Where things stand
+
+
+Most recent accepted direction: Actually wait - also add a settings panel so the user can switch cities. My test key is [REDACTED:anthropic-key] and the server is at [REDACTED:url-basic-auth]
+
+#### Accepted decisions (in order)
+
+1. Try using leaflet for an interactive radar map layer on top of the forecast.
+2. Actually wait - also add a settings panel so the user can switch cities. My test key is [REDACTED:anthropic-key] and the server is at [REDACTED:url-basic-auth]
+
+#### Constraints learned the hard way
+
+These corrections were issued during the build - do not repeat the mistakes they fixed:
+
+- No, scrap the radar map, it is too heavy. Keep the page lightweight, just the forecast cards.
+
+#### Agent memory lessons
+
+- Future agents should prefer the smallest implementation that satisfies the corrected product direction. Evidence: "No, scrap the radar map, it is too heavy. Keep the page lightweight, just the forecast cards."
+
+#### First task
+
+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).
+
+## Agent memory
+
+Project: weather-dashboard
+
+#### Durable project constraints
+
+- Keep the project local-first and privacy-first.
+- Treat the structured outputs as the core product.
+- Keep the human-readable report as one artifact among several.
+
+
+#### Lessons from this lineage
+
+- Future agents should prefer the smallest implementation that satisfies the corrected product direction. Evidence: "No, scrap the radar map, it is too heavy. Keep the page lightweight, just the forecast cards."
+
+#### Known bad paths
+
+
+- Do not narrow the project to only a README generator.
+
+#### Preferred next work
+
+- Improve the failure-signal heuristics with real fixtures.
+- Add a compare mode for baseline and candidate exports.
+
+
+## Lessons
+
+#### 1. Avoid overbuilding beyond the requested shape
+
+Future agents should prefer the smallest implementation that satisfies the corrected product direction. Evidence: "No, scrap the radar map, it is too heavy. Keep the page lightweight, just the forecast cards."
+
+Source nodes: node_002, node_003, node_004
+
+## Prompt tree
+
+> **4 prompts** · **1 session** · **1 day** · 1 correction · 1 scope change · 2 tool calls · 1 file touched
+>
+> The prompt lineage that built this project - extracted from real sessions, curated and redacted by the author, generated by [treetrace](https://github.com/REPLACE-ME-ORG/treetrace).
+
+#### Goal
+
+> Build a weather dashboard web app that shows the forecast for Memphis using the NWS API. Keep it a single static page.
+
+#### The Path
+
+`⬢` root · `→` direction · `↩` correction · `⚑` scope change · `◆` checkpoint · `?` question · `✗` abandoned
+
+- `⬢` **Build a weather dashboard web app that shows the forecast for Memphis using the NWS API.** <sub>(new session, 2026-06-01)</sub>
+ <details><summary>full prompt</summary>
+
+ > Build a weather dashboard web app that shows the forecast for Memphis using the NWS API. Keep it a single static page.
+ </details>
+- `→` Try using leaflet for an interactive radar map layer on top of the forecast.
+- `↩` No, scrap the radar map, it is too heavy.
+ <details><summary>full prompt</summary>
+
+ > No, scrap the radar map, it is too heavy. Keep the page lightweight, just the forecast cards.
+ </details>
+- `⚑` Actually wait - also add a settings panel so the user can switch cities.
+ <details><summary>full prompt</summary>
+
+ > Actually wait - also add a settings panel so the user can switch cities. My test key is [REDACTED:anthropic-key] and the server is at [REDACTED:url-basic-auth]
+ </details>
+
+#### Course corrections & dead ends
+
+**1 correction along the way:**
+
+- ↩ No, scrap the radar map, it is too heavy.
+
+#### Reusable Prompt Pack
+
+A distilled, replayable version of the accepted path - paste into a fresh agent to rebuild something like this:
+
+```text
+1. Build a weather dashboard web app that shows the forecast for Memphis using the NWS API. Keep it a single static page.
+2. Try using leaflet for an interactive radar map layer on top of the forecast.
+ (constraint learned along the way: No, scrap the radar map, it is too heavy. Keep the page lightweight, just the forecast cards.)
+3. Actually wait - also add a settings panel so the user can switch cities. My test key is [REDACTED:anthropic-key] and the server is at [REDACTED:url-basic-auth]
+```
+
+---
+
+*Generated by [treetrace](https://github.com/REPLACE-ME-ORG/treetrace) · 4 prompts across 1 session · assistant-model · machine-readable lineage in `.treetrace/tree.json` ([schema](https://github.com/REPLACE-ME-ORG/treetrace/blob/main/SCHEMA.md))*
+
+---
+
+Generated by [treetrace](https://github.com/REPLACE-ME-ORG/treetrace).
examples/weather-dashboard/tree.json +73 -3
@@ -1,13 +1,13 @@
{
- "schemaVersion": "0.1",
+ "schemaVersion": "0.2",
"generator": {
"name": "treetrace",
- "version": "0.1.0",
+ "version": "0.2.0",
"url": "https://github.com/REPLACE-ME-ORG/treetrace"
},
"project": {
"name": "weather-dashboard",
- "generatedAt": "2026-06-12T00:01:29.681Z",
+ "generatedAt": "2026-06-12T02:37:21.277Z",
"sourceType": "claude-code-jsonl"
},
"stats": {
@@ -26,6 +26,12 @@
"firstTs": "2026-06-01T10:00:00.000Z",
"lastTs": "2026-06-01T10:12:00.000Z"
},
+ "analysis": {
+ "failureSignals": 1,
+ "correctionChains": 1,
+ "evalCandidates": 1,
+ "lessons": 1
+ },
"sessions": [
{
"id": "synthetic-session",
@@ -49,6 +55,9 @@
"reruns": 0,
"session": "synthetic-session",
"timestamp": "2026-06-01T10:00:00.000Z",
+ "failureSignals": [],
+ "evalCandidate": false,
+ "lessonIds": [],
"sourceEventIds": [
"u1"
]
@@ -65,6 +74,18 @@
"reruns": 0,
"session": "synthetic-session",
"timestamp": "2026-06-01T10:04:00.000Z",
+ "failureSignals": [
+ {
+ "type": "overbuilt_solution",
+ "confidence": 0.78,
+ "evidence": "User said: \"No, scrap the radar map, it is too heavy. Keep the page lightweight, just the forecast cards.\"",
+ "resolvedBy": "node_003"
+ }
+ ],
+ "evalCandidate": true,
+ "lessonIds": [
+ "lesson_001"
+ ],
"sourceEventIds": [
"u5"
]
@@ -81,6 +102,9 @@
"reruns": 0,
"session": "synthetic-session",
"timestamp": "2026-06-01T10:09:00.000Z",
+ "failureSignals": [],
+ "evalCandidate": false,
+ "lessonIds": [],
"sourceEventIds": [
"u7"
]
@@ -97,6 +121,9 @@
"reruns": 0,
"session": "synthetic-session",
"timestamp": "2026-06-01T10:12:00.000Z",
+ "failureSignals": [],
+ "evalCandidate": false,
+ "lessonIds": [],
"sourceEventIds": [
"u9"
]
@@ -118,5 +145,48 @@
"to": "node_004",
"relationship": "expands"
}
+ ],
+ "correctionChains": [
+ {
+ "id": "chain_001",
+ "failureNodeId": "node_002",
+ "correctionNodeId": "node_003",
+ "resolvedNodeId": "node_004",
+ "failureType": "overbuilt_solution",
+ "confidence": "medium",
+ "summary": "The work appears to have overbuilt the requested shape near \"Try using leaflet for an interactive radar map layer on top of the forecast.\"; corrected by \"No, scrap the radar map, it is too heavy.\"."
+ }
+ ],
+ "lessons": [
+ {
+ "id": "lesson_001",
+ "title": "Avoid overbuilding beyond the requested shape",
+ "nodeIds": [
+ "node_002",
+ "node_003",
+ "node_004"
+ ],
+ "text": "Future agents should prefer the smallest implementation that satisfies the corrected product direction. Evidence: \"No, scrap the radar map, it is too heavy. Keep the page lightweight, just the forecast cards.\""
+ }
+ ],
+ "evalCandidates": [
+ {
+ "id": "eval_001",
+ "source": "treetrace",
+ "type": "scope_drift_detection",
+ "task": "Continue development while preserving the corrected direction from the session lineage.",
+ "context": "The work appears to have overbuilt the requested shape near \"Try using leaflet for an interactive radar map layer on top of the forecast.\"; corrected by \"No, scrap the radar map, it is too heavy.\".",
+ "input": "Continue development of the project while preserving the corrected direction and constraints.",
+ "expected_behavior": [
+ "Use the corrected prompt lineage as durable context",
+ "Do not repeat the documented failure mode"
+ ],
+ "failure_mode": "Agent repeats overbuilt solution despite prior correction.",
+ "sourceNodeIds": [
+ "node_002",
+ "node_003",
+ "node_004"
+ ]
+ }
]
}
src/cli.js +98 -6
@@ -8,23 +8,38 @@ import { scanText, resolveFindings, applyDecisions, shadowScan } from './redact.
import { renderMarkdown } from './render-md.js';
import { renderJson } from './render-json.js';
import { renderHandoff } from './handoff.js';
+import { renderReportMarkdown, renderTerminalSummary } from './report.js';
+import {
+ analyzeTree,
+ renderFailuresJson,
+ renderLessonsMarkdown,
+ renderEvalsJsonl,
+ renderMemoryMarkdown,
+} from './analyze.js';
import { makeTitle } from './extract.js';
import { c, plural, truncate } from './util.js';
-const VERSION = '0.1.0';
+const VERSION = '0.2.0';
-const HELP = `treetrace - turn AI coding sessions into a shareable PROMPT_TREE.md
+const HELP = `TreeTrace - turn AI coding sessions into regression-ready prompt lineage
Usage:
treetrace auto-discover Claude Code sessions for this directory
treetrace --file <path>... parse specific transcript files (.jsonl or plain text)
treetrace --stdin read a pasted transcript from stdin
+ treetrace --report write all artifacts and print the human report
treetrace --handoff print an agent-ready handoff brief to stdout
+ treetrace --failures write and print failure-analysis JSON
+ treetrace --lessons write and print lessons Markdown
+ treetrace --evals write and print eval JSONL
+ treetrace --memory write and print compact agent memory
Options:
--dir <path> project directory to trace (default: cwd)
--out <file> markdown output path (default: PROMPT_TREE.md)
+ --report-file <file> human report output path (default: TREETRACE_REPORT.md)
--json also print lineage JSON to stdout
+ --analysis write failure, lesson, eval, and memory artifacts
--titles-only omit full prompt texts from the markdown tree
--redact-auto redact every detected secret without prompting
--since <YYYY-MM-DD> only include sessions active on/after this date
@@ -33,7 +48,7 @@ Options:
Every export passes a redaction gate: detected secrets must be resolved
(redact/keep/edit) before anything is written. Outside a terminal, every
-hit is redacted automatically - treetrace fails closed.`;
+hit is redacted automatically - treetrace fails closed.`;
export async function main(argv) {
const opts = parseArgs(argv);
@@ -119,6 +134,7 @@ export async function main(argv) {
node.text = applyDecisions(node.text, findings, decisions);
if (node.text !== before) node.title = makeTitle(node.text);
}
+ analyzeTree(tree);
// ---- render ----
const generatedAt = new Date().toISOString();
@@ -135,28 +151,90 @@ export async function main(argv) {
const md = renderMarkdown(tree, renderOpts);
const json = renderJson(tree, renderOpts);
const jsonText = JSON.stringify(json, null, 2);
+ const artifacts = analysisArtifacts(ttDir, tree, renderOpts);
+ const outPath = resolve(projectDir, opts.out || 'PROMPT_TREE.md');
+ const reportPath = resolve(projectDir, opts.reportFile || 'TREETRACE_REPORT.md');
+ const report = renderReportMarkdown(tree, renderOpts);
+
+ const requested = requestedArtifacts(opts, artifacts);
+ if (requested.length) {
+ for (const artifact of requested) assertClean(artifact.text, decisions, artifact.label);
+ mkdirSync(ttDir, { recursive: true });
+ for (const artifact of requested) writeFileSync(artifact.path, artifact.text);
+ writeFileSync(decisionsPath, JSON.stringify(decisions, null, 2));
+ if (requested.length === 1) {
+ process.stdout.write(requested[0].text);
+ } else {
+ process.stdout.write(requested.map((a) => `# ${a.label}\n\n${a.text}`).join('\n'));
+ }
+ log(c.green(`wrote ${requested.map((a) => relativeish(a.path, projectDir)).join(', ')}`));
+ return;
+ }
assertClean(md, decisions, 'PROMPT_TREE.md');
assertClean(jsonText, decisions, 'tree.json');
+ for (const artifact of Object.values(artifacts)) assertClean(artifact.text, decisions, artifact.label);
+ assertClean(report, decisions, 'TREETRACE_REPORT.md');
- const outPath = resolve(projectDir, opts.out || 'PROMPT_TREE.md');
writeFileSync(outPath, md);
+ writeFileSync(reportPath, report);
mkdirSync(ttDir, { recursive: true });
writeFileSync(join(ttDir, 'tree.json'), jsonText);
- // decisions file stores only hashes + actions - safe to keep, never secrets
+ for (const artifact of Object.values(artifacts)) writeFileSync(artifact.path, artifact.text);
+ // decisions file stores only hashes + actions - safe to keep, never secrets
writeFileSync(decisionsPath, JSON.stringify(decisions, null, 2));
if (opts.json) process.stdout.write(jsonText + '\n');
+ if (opts.report) process.stdout.write(report);
// ---- terminal summary ----
log('');
log(summaryLine(tree.stats, projectName));
+ log(renderTerminalSummary(tree, renderOpts).trimEnd());
previewTree(tree, log);
log('');
- log(`${c.green('✓')} wrote ${c.bold(relativeish(outPath, projectDir))} and .treetrace/tree.json`);
+ log(
+ `${c.green('ok')} wrote ${c.bold(relativeish(reportPath, projectDir))}, ${c.bold(relativeish(outPath, projectDir))}, .treetrace/tree.json, and analysis artifacts`
+ );
+ if (!opts.report) log(c.dim(' run `treetrace --report` to print the human report in this terminal'));
if (asked) log(c.dim(` ${plural(asked, 'redaction decision')} saved to .treetrace/redactions.json`));
}
+function analysisArtifacts(ttDir, tree, renderOpts) {
+ return {
+ failures: {
+ label: 'failures.json',
+ path: join(ttDir, 'failures.json'),
+ text: JSON.stringify(renderFailuresJson(tree, renderOpts), null, 2),
+ },
+ lessons: {
+ label: 'lessons.md',
+ path: join(ttDir, 'lessons.md'),
+ text: renderLessonsMarkdown(tree, renderOpts),
+ },
+ evals: {
+ label: 'evals.jsonl',
+ path: join(ttDir, 'evals.jsonl'),
+ text: renderEvalsJsonl(tree, renderOpts),
+ },
+ memory: {
+ label: 'agent-memory.md',
+ path: join(ttDir, 'agent-memory.md'),
+ text: renderMemoryMarkdown(tree, renderOpts),
+ },
+ };
+}
+
+function requestedArtifacts(opts, artifacts) {
+ const requested = [];
+ if (opts.failures) requested.push(artifacts.failures);
+ if (opts.lessons) requested.push(artifacts.lessons);
+ if (opts.evals) requested.push(artifacts.evals);
+ if (opts.memory) requested.push(artifacts.memory);
+ if (opts.analysis && !requested.length) requested.push(...Object.values(artifacts));
+ return requested;
+}
+
function assertClean(rendered, decisions, label) {
const leaks = shadowScan(rendered, decisions);
if (leaks.length) {
@@ -234,8 +312,14 @@ function parseArgs(argv) {
const opts = {
files: [],
stdin: false,
+ report: false,
handoff: false,
json: false,
+ analysis: false,
+ failures: false,
+ lessons: false,
+ evals: false,
+ memory: false,
titlesOnly: false,
redactAuto: false,
quiet: false,
@@ -243,6 +327,7 @@ function parseArgs(argv) {
version: false,
dir: null,
out: null,
+ reportFile: null,
since: null,
};
for (let i = 0; i < argv.length; i++) {
@@ -252,8 +337,14 @@ function parseArgs(argv) {
while (argv[i + 1] && !argv[i + 1].startsWith('--')) opts.files.push(argv[++i]);
break;
case '--stdin': opts.stdin = true; break;
+ case '--report': opts.report = true; break;
case '--handoff': opts.handoff = true; break;
case '--json': opts.json = true; break;
+ case '--analysis': opts.analysis = true; break;
+ case '--failures': opts.failures = true; break;
+ case '--lessons': opts.lessons = true; break;
+ case '--evals': opts.evals = true; break;
+ case '--memory': opts.memory = true; break;
case '--titles-only': opts.titlesOnly = true; break;
case '--redact-auto': opts.redactAuto = true; break;
case '--quiet': opts.quiet = true; break;
@@ -261,6 +352,7 @@ function parseArgs(argv) {
case '--version': case '-v': opts.version = true; break;
case '--dir': opts.dir = argv[++i]; break;
case '--out': opts.out = argv[++i]; break;
+ case '--report-file': opts.reportFile = argv[++i]; break;
case '--since': opts.since = argv[++i]; break;
default:
throw new Error(`unknown option ${a} (try --help)`);
src/report.js +146 -0
@@ -0,0 +1,146 @@
+import { analyzeTree, renderLessonsMarkdown, renderMemoryMarkdown } from './analyze.js';
+import { renderHandoff } from './handoff.js';
+import { renderMarkdown } from './render-md.js';
+import { plural, truncate } from './util.js';
+import { REPO_URL } from './config.js';
+
+export function renderReportMarkdown(tree, opts = {}) {
+ const projectName = opts.projectName || 'project';
+ const generatedAt = opts.generatedAt || new Date().toISOString();
+ const analysis = analyzeTree(tree);
+ const lines = [];
+
+ lines.push(`# TreeTrace Report - ${projectName}`);
+ 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('');
+ lines.push(`- Prompts: ${tree.stats.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}`);
+ lines.push(`- Failure signals: ${analysis.summary.totalFailureSignals}`);
+ lines.push(`- Eval candidates: ${analysis.summary.evalCandidates}`);
+ lines.push(`- Lessons: ${analysis.summary.lessons}`);
+ lines.push('');
+
+ 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/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('');
+
+ if (analysis.failures.length) {
+ lines.push('## Failure signals');
+ lines.push('');
+ for (const { type, count } of analysis.summary.topFailureTypes) {
+ lines.push(`- ${type}: ${count}`);
+ }
+ lines.push('');
+ for (const failure of analysis.failures.slice(0, 8)) {
+ lines.push(`- ${failure.id} (${failure.type}, ${confidencePct(failure.confidence)}): ${failure.summary}`);
+ }
+ if (analysis.failures.length > 8) {
+ lines.push(`- ... ${analysis.failures.length - 8} more in .treetrace/failures.json`);
+ }
+ 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('');
+
+ lines.push('---');
+ lines.push('');
+ lines.push(`Generated by [treetrace](${REPO_URL}).`);
+ lines.push('');
+
+ return lines.join('\n');
+}
+
+export function renderTerminalSummary(tree, opts = {}) {
+ const projectName = opts.projectName || 'project';
+ const analysis = analyzeTree(tree);
+ const accepted = tree.nodes.filter((n) => n.status !== 'abandoned');
+ const lastAccepted = accepted.at(-1);
+ const lines = [];
+
+ lines.push(`TreeTrace summary - ${projectName}`);
+ lines.push('');
+ lines.push(
+ `${plural(tree.stats.promptCount, 'prompt')} across ${plural(tree.stats.sessionCount, 'session')} ` +
+ `| ${analysis.summary.totalFailureSignals} failure signals ` +
+ `| ${analysis.summary.lessons} lessons ` +
+ `| ${analysis.summary.evalCandidates} eval candidates`
+ );
+ if (lastAccepted) {
+ lines.push('');
+ lines.push(`Latest accepted direction: ${truncate(lastAccepted.text.replace(/\s+/g, ' '), 280)}`);
+ }
+ if (analysis.lessons.length) {
+ lines.push('');
+ lines.push('Top lessons:');
+ for (const lesson of analysis.lessons.slice(0, 3)) {
+ lines.push(`- ${truncate(lesson.text.replace(/\s+/g, ' '), 240)}`);
+ }
+ }
+ lines.push('');
+ lines.push('Human report: TREETRACE_REPORT.md');
+ lines.push('Stream it in the terminal with: treetrace --report');
+ lines.push('');
+
+ 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)}%`;
+}