codecartographer-pi 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.codecarto/GUIDE.md +40 -18
  2. package/.codecarto/README.md +3 -0
  3. package/.codecarto/findings/goal-synthesis/README.md +3 -0
  4. package/.codecarto/findings/goal-synthesis-finalize/SKILL.md +30 -0
  5. package/.codecarto/findings/goal-synthesis-propose/SKILL.md +24 -0
  6. package/.codecarto/findings/porting/SKILL.md +7 -0
  7. package/.codecarto/findings/reimplementation-spec/SKILL.md +10 -0
  8. package/.codecarto/findings/spec-merge/README.md +3 -0
  9. package/.codecarto/findings/spec-merge/SKILL.md +23 -0
  10. package/.codecarto/findings/vision-capture/README.md +3 -0
  11. package/.codecarto/findings/vision-capture/SKILL.md +26 -0
  12. package/.codecarto/inputs/vision.md +11 -0
  13. package/.codecarto/templates/architecture-map.md +9 -0
  14. package/.codecarto/templates/behavioral-contracts.md +9 -0
  15. package/.codecarto/templates/defect-report.md +9 -0
  16. package/.codecarto/templates/mechanical-defects.md +9 -0
  17. package/.codecarto/templates/merged-spec.md +58 -0
  18. package/.codecarto/templates/phase-checkpoint.md +41 -0
  19. package/.codecarto/templates/phase-handoff.yaml +22 -0
  20. package/.codecarto/templates/project-plan.md +70 -0
  21. package/.codecarto/templates/proposal.md +43 -0
  22. package/.codecarto/templates/protocols-and-state.md +9 -0
  23. package/.codecarto/templates/reimplementation-spec-opinionated.md +10 -0
  24. package/.codecarto/templates/reimplementation-spec.md +10 -0
  25. package/.codecarto/templates/reverse-engineering-bundle.md +29 -3
  26. package/.codecarto/templates/semantic-defects.md +9 -0
  27. package/.codecarto/templates/vision.md +63 -0
  28. package/.codecarto/workflow/pipeline-architecture-only.yaml +1 -0
  29. package/.codecarto/workflow/pipeline-defect-scan.yaml +2 -0
  30. package/.codecarto/workflow/pipeline-full-with-audit.yaml +8 -3
  31. package/.codecarto/workflow/pipeline-full-with-deep-audit.yaml +9 -5
  32. package/.codecarto/workflow/pipeline-lite.yaml +3 -0
  33. package/.codecarto/workflow/pipeline-synthesis.yaml +105 -0
  34. package/.codecarto/workflow/pipeline.yaml +7 -3
  35. package/.codecarto/workflow/status.yaml +2 -0
  36. package/README.md +89 -7
  37. package/dist/core/completion.d.ts +6 -0
  38. package/dist/core/completion.js +127 -0
  39. package/dist/core/dashboard.js +37 -7
  40. package/dist/core/index.d.ts +2 -0
  41. package/dist/core/index.js +2 -0
  42. package/dist/core/pipeline.js +1 -0
  43. package/dist/core/prompts.d.ts +9 -3
  44. package/dist/core/prompts.js +43 -26
  45. package/dist/core/status.d.ts +7 -1
  46. package/dist/core/status.js +187 -2
  47. package/dist/core/synthesis.d.ts +31 -0
  48. package/dist/core/synthesis.js +140 -0
  49. package/dist/core/types.d.ts +29 -1
  50. package/dist/core/usage.d.ts +11 -0
  51. package/dist/core/usage.js +64 -46
  52. package/dist/core/workspace.d.ts +3 -1
  53. package/dist/core/workspace.js +39 -3
  54. package/dist/core/yaml.js +24 -0
  55. package/dist/extensions/codecarto/agent-rewriter.js +0 -1
  56. package/dist/extensions/codecarto/agent-runner.d.ts +19 -0
  57. package/dist/extensions/codecarto/agent-runner.js +68 -6
  58. package/dist/extensions/codecarto/agent-state.d.ts +3 -0
  59. package/dist/extensions/codecarto/agent-state.js +2 -0
  60. package/dist/extensions/codecarto/agent-summary.d.ts +5 -0
  61. package/dist/extensions/codecarto/agent-summary.js +9 -0
  62. package/dist/extensions/codecarto/agent-widget.js +6 -0
  63. package/dist/extensions/codecarto/auto-runner.d.ts +3 -1
  64. package/dist/extensions/codecarto/auto-runner.js +33 -69
  65. package/dist/extensions/codecarto/dashboard-narrator.js +0 -1
  66. package/dist/extensions/codecarto/index.d.ts +1 -1
  67. package/dist/extensions/codecarto/index.js +153 -12
  68. package/dist/extensions/codecarto/phase-compaction.d.ts +11 -0
  69. package/dist/extensions/codecarto/phase-compaction.js +115 -0
  70. package/dist/mcp-server/server.js +24 -68
  71. package/package.json +4 -3
@@ -1,4 +1,5 @@
1
1
  project_name: ""
2
+ schema_version: 1
2
3
  # source_location is defined in the active pipeline YAML. Do not duplicate it here.
3
4
  pipeline: workflow/pipeline-full-with-deep-audit.yaml
4
5
  # ^^^ To switch pipelines, change the line above AND adjust the phases below to match:
@@ -62,3 +63,4 @@ phases:
62
63
  carry_forward: []
63
64
  next_actions:
64
65
  - Begin architecture phase by reading the repository and producing findings/architecture/architecture-map.md
66
+ post_pipeline: []
package/README.md CHANGED
@@ -32,8 +32,11 @@
32
32
  | **HTML dashboard** — single-file aggregate of progress, links, usage, narrative | `.codecarto/dashboard.html` |
33
33
  | **Per-phase token tracking** | `/codecarto-usage` |
34
34
  | **Opt-in LLM steering** of the next phase's seed prompt | `/codecarto-next --llm-steer` |
35
+ | **Forward synthesis** — vision + confirmed library specs → provenance-backed project plan | `pipeline-synthesis.yaml` |
35
36
 
36
- > **Forward-flow synthesis is underway.** v0.9.0 adds the experimental library foundation and MCP publish/list/reindex tools for accumulating `reimplementation-spec.md` artifacts in a git-trackable library. The Pi publish UX and synthesis pipeline that turns selected library entries plus a vision into `project-plan.md` are still in progress. See [`docs/synthesis-roadmap.md`](docs/synthesis-roadmap.md) for the implementation tracker.
37
+ > **Forward-flow synthesis is available on the development branch.** Publish completed reimplementation specs from Pi or MCP, then run the `synthesis` pipeline to turn a product vision and explicitly confirmed library entries into a conflict-aware `project-plan.md` with a decision-level provenance ledger.
38
+
39
+ OpenAI Build Week reviewers: see the [new-vs-existing scope and one-command demo](docs/build-week-2026.md).
37
40
 
38
41
  ---
39
42
 
@@ -93,7 +96,47 @@ cp -r /path/to/CodeCartographer/.codecarto /path/to/your-repo/
93
96
 
94
97
  Then in the LLM session: `Read .codecarto/GUIDE.md and begin the analysis.`
95
98
 
96
- > **Limitation.** Drop-in mode runs the analysis pipeline fully, but library + synthesis workflows require executable code. Publishing and reading library entries are currently available through the MCP server; Pi publish UX and project-plan synthesis are still in progress. See [`docs/synthesis-roadmap.md`](docs/synthesis-roadmap.md) for the planned scope.
99
+ > **Limitation.** Drop-in mode runs the analysis pipeline fully, but library + synthesis workflows require executable code through Pi or MCP.
100
+
101
+ ---
102
+
103
+ ## Forward synthesis quickstart
104
+
105
+ Analysis turns repositories into reusable specifications. Synthesis runs the other direction: it combines a raw product vision with human-confirmed specifications and produces an implementation-ready plan without losing provenance.
106
+
107
+ 1. Configure the library that contains specs published with `/codecarto-publish` or the MCP `codecarto_publish` tool:
108
+
109
+ ```yaml
110
+ # ~/.codecarto/config.yaml or .codecarto/workflow/config.yaml
111
+ library:
112
+ path: /absolute/path/to/codecarto-library
113
+ namespace: your-namespace # omit for a single-tenant library
114
+ publish_confirm: true
115
+ ```
116
+
117
+ 2. Initialize a clean planning workspace and fill in its brief:
118
+
119
+ ```text
120
+ /codecarto-init synthesis
121
+ ```
122
+
123
+ Edit `.codecarto/inputs/vision.md` with the audience, problem, desired outcome, constraints, and non-goals.
124
+
125
+ 3. Run until CodeCartographer creates the candidate proposal:
126
+
127
+ ```text
128
+ /codecarto-next --auto
129
+ ```
130
+
131
+ The run intentionally stops before merging. Review `.codecarto/findings/goal-synthesis/proposal.md` and change one or more candidate boxes from `[ ]` to `[x]`.
132
+
133
+ 4. Resume:
134
+
135
+ ```text
136
+ /codecarto-next --auto
137
+ ```
138
+
139
+ The final `.codecarto/findings/goal-synthesis/project-plan.md` contains product scope, architecture, work packages, acceptance gates, an unresolved-conflict register, and a provenance ledger mapping every load-bearing decision back to the vision or a confirmed specification. Runtime preflight checks prevent merging or finalization before explicit human confirmation.
97
140
 
98
141
  ---
99
142
 
@@ -114,6 +157,39 @@ For multi-session work, every new session reads `.codecarto/GUIDE.md` (or the li
114
157
 
115
158
  ---
116
159
 
160
+ ## Progressive distillation and context resilience
161
+
162
+ CodeCartographer is a progressive, evidence-tagged distillation of a codebase. It does not ask one context window to retain the entire investigation. Instead, each phase turns a large body of source evidence into a smaller, more task-specific artifact that the next phase can read:
163
+
164
+ ```text
165
+ source code
166
+ → architecture map
167
+ → behavioral contracts + protocols + defect findings
168
+ → porting bundle
169
+ → reimplementation spec
170
+ ```
171
+
172
+ This is deliberate distillation, not incidental chat summarization. Each artifact follows a template, preserves evidence levels and known unknowns, and must pass validation before it becomes an input to downstream phases.
173
+
174
+ ### What happens when conversation context is compacted?
175
+
176
+ The filesystem, not the conversation, is the durable memory of a run:
177
+
178
+ - Each phase gets a fresh context window. In the Pi extension it runs as an isolated phase sub-agent; MCP and drop-in hosts should use the same one-session-per-phase pattern.
179
+ - Completed findings live under `.codecarto/findings/`. Later phases re-read the specific upstream artifacts declared by the active pipeline instead of relying on conversational recall.
180
+ - `workflow/status.yaml` records progress, terminal `open_questions`, in-pipeline `carry_forward`, and a separate `post_pipeline` backlog for optional spikes, amendments, deltas, decisions, and reruns after completion. Phase agents propose changes in `.codecarto/scratch/handoffs/<phase>.yaml`; completion validates and applies them under a lock with host timestamps, one canonical closeout, and an idempotent `THREAD_LOG.md` entry.
181
+ - Pi phase transcripts are file-backed and remain available through `/resume`, `/tree`, and `/export`, even when the active model context has been compacted.
182
+ - For isolated Pi phase sessions, compaction uses a phase-aware continuation summary that explicitly preserves evidence, files inspected, output progress, open questions, and validation gaps. The resulting summary is also checkpointed atomically at `.codecarto/scratch/checkpoints/<phase>.md`.
183
+ - Pi records successful, failed, and aborted compactions plus their trigger (`threshold`, `overflow`, or `manual`) in local usage data and exposes the totals in the widget, `/codecarto-usage`, completion summaries, and dashboard.
184
+
185
+ As a result, compaction—or even replacement—of the orchestrator session does not erase pipeline progress. A new session can reconstruct the relevant state from disk and continue.
186
+
187
+ The remaining limit is **within a single oversized phase**. Even Pi's phase-aware summary is still a lossy distillation, and MCP/drop-in compaction remains entirely host-controlled. Phase instructions therefore prioritize targeted reads, durable checkpoints, and explicit coverage accounting; if full coverage will not fit, the phase records `PARTIAL` validation and places unresolved work in `open_questions` or `carry_forward`. Cross-phase context loss is largely designed out; intra-phase context pressure is observed and bounded rather than hidden.
188
+
189
+ The porting bundle is the final intentional compression boundary. It carries a source index, load-bearing invariants, defect dispositions, and deep-read triggers. `reimplementation-spec` reads that bundle by default and opens lower-level reports only for a named gap, conflict, missing acceptance detail, or defect rationale.
190
+
191
+ ---
192
+
117
193
  ## Phases produce these artifacts
118
194
 
119
195
  | Artifact | Description |
@@ -142,6 +218,7 @@ The default is a 7-phase run that splits the defect scan into a mechanical early
142
218
  | **Defect scan** | 2 | Maintenance audit to surface latent problems |
143
219
  | **Lite** | 3 | You need to understand behavior without porting plans |
144
220
  | **Architecture only** | 1 | Quick structural overview |
221
+ | **Synthesis** | 4 | Turn a product vision and confirmed library specifications into a provenance-backed implementation plan |
145
222
 
146
223
  Set the active pipeline by editing `workflow/status.yaml`'s `pipeline:` field, or pass it as the argument to `/codecarto-init`.
147
224
 
@@ -155,6 +232,7 @@ Set the active pipeline by editing `workflow/status.yaml`'s `pipeline:` field, o
155
232
  | Defect scan | `workflow/pipeline-defect-scan.yaml` |
156
233
  | Lite | `workflow/pipeline-lite.yaml` |
157
234
  | Architecture only | `workflow/pipeline-architecture-only.yaml` |
235
+ | Synthesis | `workflow/pipeline-synthesis.yaml` |
158
236
 
159
237
  ---
160
238
 
@@ -164,7 +242,7 @@ Every state change re-renders `.codecarto/dashboard.html` — a self-contained s
164
242
 
165
243
  - Pipeline progress strip with per-phase status badges
166
244
  - Per-phase cards with output links, open questions, carry-forward routing, owner notes, last-run usage
167
- - Aggregate token usage panel + per-phase breakdown
245
+ - Aggregate token and compaction telemetry + per-phase breakdown
168
246
  - Activity timeline with session-file links
169
247
  - Open questions roll-up grouped by source phase
170
248
  - Closeouts list (reverse-chronological) with relative-path links
@@ -191,25 +269,29 @@ Beyond the slash commands, the Pi extension layers on:
191
269
 
192
270
  **File-backed phase sessions.** Phase transcripts persist to the same Pi session directory the orchestrator uses, so `/resume`, `/tree`, and `/export` browse them as first-class sessions. Each appears as `CodeCartographer phase: <id>` with lineage back to the orchestrator's session.
193
271
 
272
+ **Phase-aware compaction and checkpoints.** Only isolated sessions named `CodeCartographer phase: <id>` receive the specialized compaction prompt. It preserves the phase goal, evidence, inspected files, output progress, open questions, and validation gaps, then writes the resulting summary to `.codecarto/scratch/checkpoints/<phase>.md`. Orchestrator and unrelated Pi sessions retain normal host compaction.
273
+
194
274
  **Phase-completion summary in the orchestrator transcript.** When a phase finishes, a Markdown closeout block is appended to the orchestrator's session via `pi.sendMessage(...)`. Visible in the TUI scrollback; available to the orchestrator's LLM as context on your next message. No auto-trigger — you stay in control.
195
275
 
196
276
  **Opt-in LLM-steered seed prompts.** Set `orchestrator.llm_steer_next_phase: true` in `.codecarto/workflow/config.yaml` (or pass `--llm-steer` per invocation), and the orchestrator's LLM rewrites the next phase's seed prompt to highlight relevant prior findings. Off by default — extra orchestrator-side tokens, opt-in. The rewritten prompt is injected into the orchestrator transcript so you can audit what the rewriter chose to emphasize.
197
277
 
198
- **Per-phase usage tracking.** Each phase run is appended to `.codecarto/workflow/.usage.local.yaml`. `/codecarto-usage` reports cumulative + per-phase totals.
278
+ **Per-phase usage tracking.** Each phase run is appended to `.codecarto/workflow/.usage.local.yaml`. `/codecarto-usage` reports cumulative + per-phase token, runtime, tool-use, and compaction totals, including threshold/overflow/manual triggers and successful/failed/aborted outcomes.
199
279
 
200
- **Tool interception.** `bash` is blocked outright; `edit` and `write` are confined to `.codecarto/`. Same rules apply to phase sub-agents.
280
+ **Tool interception.** `bash` is blocked outright; `edit` and `write` are confined to `.codecarto/`, plus the configured, marker-validated CodeCartographer library when one is configured. Same rules apply to phase sub-agents.
201
281
 
202
282
  ### Slash commands
203
283
 
204
284
  | Command | Purpose |
205
285
  |---|---|
206
286
  | `/codecarto-init [variant]` | Copy `.codecarto/` into the current repository, select pipeline variant |
287
+ | `/codecarto-open` | Activate an existing `.codecarto/` workspace in a new Pi session without resetting durable state |
207
288
  | `/codecarto-status` | Current phase, progress, open questions |
208
289
  | `/codecarto-next [--auto [--strict]] [--llm-steer \| --no-llm-steer]` | Spawn the next eligible phase as a sub-agent. `--auto` walks the full pipeline end-to-end (auto-validate + auto-complete + advance); `--strict` flips the `PASS WITH GAPS` rule from "advance" to "pause". |
209
290
  | `/codecarto-phase <id>` | Force a specific phase, even out of pipeline order |
210
291
  | `/codecarto-validate [phase]` | Validate a phase output against completion criteria |
211
- | `/codecarto-complete [phase]` | Atomically mark a phase complete (validation must pass) |
292
+ | `/codecarto-complete [phase]` | Validate and atomically apply the phase handoff, canonical status, closeout, and log entry |
212
293
  | `/codecarto-skill <name>` | Run a post-pipeline skill once all phases are complete |
294
+ | `/codecarto-publish` | Publish the reimplementation spec to the configured library after reviewing an explicit confirmation preview |
213
295
  | `/codecarto-usage` | Cumulative + per-phase token usage |
214
296
  | `/codecarto-dashboard [--narrate]` | Regenerate `.codecarto/dashboard.html`; `--narrate` for the LLM executive summary |
215
297
 
@@ -391,7 +473,7 @@ If you're testing a new model, start with `pipeline-architecture-only.yaml` on a
391
473
  protocols/ # Event streams, state machines, persistence formats.
392
474
  porting/ # Reverse-engineering synthesis bundle.
393
475
  reimplementation-spec/ # Language-agnostic build spec.
394
- scratch/ # Disposable analysis notes.
476
+ scratch/ # Disposable notes plus checkpoints and structured phase handoffs.
395
477
  templates/ # Output structure templates.
396
478
  workflow/ # Pipeline definitions, status, validation, config.
397
479
  closeouts/ # Per-session closeout files.
@@ -0,0 +1,6 @@
1
+ import type { ValidationResult, WorkspaceState } from "./types.ts";
2
+ export type CompletionResult = {
3
+ updatedState: WorkspaceState;
4
+ closeoutNotice?: string;
5
+ };
6
+ export declare function completeValidatedPhase(cwd: string, validation: ValidationResult, sourceLabel: string): Promise<CompletionResult>;
@@ -0,0 +1,127 @@
1
+ import { appendFile, copyFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { getNextEligiblePhase, resolvePhase } from "./pipeline.js";
4
+ import { applyHandoff, autoAssignIds, loadHandoffFile, normalizeStatus } from "./status.js";
5
+ import { dateOnly, pathExists, uniqueStrings } from "./utils.js";
6
+ import { getWorkspaceState, updateStatusAtomically } from "./workspace.js";
7
+ function escapeRegExp(value) {
8
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9
+ }
10
+ async function canonicalCloseoutFile(workspaceDir, phaseId, timestamp) {
11
+ const closeoutsDir = join(workspaceDir, "closeouts");
12
+ await mkdir(closeoutsDir, { recursive: true });
13
+ const pattern = new RegExp(`^\\d{4}-\\d{2}-\\d{2}-${escapeRegExp(phaseId)}\\.md$`);
14
+ const existing = (await readdir(closeoutsDir)).filter((name) => pattern.test(name)).sort();
15
+ return existing.at(-1) ?? `${dateOnly(timestamp)}-${phaseId}.md`;
16
+ }
17
+ async function writeCompletionArtifacts(workspaceDir, phaseId, validation, timestamp, handoff) {
18
+ const closeoutFile = await canonicalCloseoutFile(workspaceDir, phaseId, timestamp);
19
+ const closeoutPath = join(workspaceDir, "closeouts", closeoutFile);
20
+ const suppliedContent = handoff?.closeout_content?.trim();
21
+ if (suppliedContent) {
22
+ const decisions = handoff?.decisions ?? [];
23
+ const decisionsSection = decisions.length > 0
24
+ ? `\n\n## Decisions Beyond Prompt\n\n${decisions.map((decision) => `- ${decision}`).join("\n")}`
25
+ : "";
26
+ await writeFile(closeoutPath, `${suppliedContent}${decisionsSection}\n`, "utf8");
27
+ }
28
+ else if (!(await pathExists(closeoutPath))) {
29
+ const templatePath = join(workspaceDir, "templates", "closeout-template.md");
30
+ if (await pathExists(templatePath))
31
+ await copyFile(templatePath, closeoutPath);
32
+ }
33
+ const summary = handoff?.closeout_summary?.trim() || `Validation: ${validation.overall}`;
34
+ const entry = `- ${dateOnly(timestamp)} — ${phaseId} — ${summary} — [closeout](closeouts/${closeoutFile})`;
35
+ const threadLogPath = join(workspaceDir, "THREAD_LOG.md");
36
+ let current = "";
37
+ try {
38
+ current = await readFile(threadLogPath, "utf8");
39
+ }
40
+ catch {
41
+ // Created below when absent.
42
+ }
43
+ const link = `[closeout](closeouts/${closeoutFile})`;
44
+ if (!current.split(/\r?\n/).some((line) => line.includes(link))) {
45
+ await appendFile(threadLogPath, `${entry}\n`, "utf8");
46
+ }
47
+ return `.codecarto/closeouts/${closeoutFile}`;
48
+ }
49
+ export async function completeValidatedPhase(cwd, validation, sourceLabel) {
50
+ const initialState = await getWorkspaceState(cwd);
51
+ if (!initialState)
52
+ throw new Error("CodeCartographer workspace not found. Run /codecarto-init first.");
53
+ const handoff = await loadHandoffFile(validation.phaseId, initialState.workspaceDir);
54
+ if (handoff && handoff.phase_id !== validation.phaseId) {
55
+ throw new Error(`Invalid handoff: phase_id ${handoff.phase_id} does not match ${validation.phaseId}`);
56
+ }
57
+ if (handoff) {
58
+ const activePhases = new Set(initialState.pipeline.phase_order);
59
+ const sourceIndex = initialState.pipeline.phase_order.indexOf(validation.phaseId);
60
+ for (const entry of handoff.carry_forward) {
61
+ const targetIndex = entry.target_phase ? initialState.pipeline.phase_order.indexOf(entry.target_phase) : -1;
62
+ if (!entry.target_phase || !activePhases.has(entry.target_phase) || targetIndex <= sourceIndex) {
63
+ throw new Error(`Invalid handoff: carry_forward target_phase ${entry.target_phase ?? "(missing)"} is not a downstream active pipeline phase; use post_pipeline for work after the pipeline`);
64
+ }
65
+ }
66
+ for (const entry of handoff.post_pipeline) {
67
+ if (!entry.id?.trim())
68
+ throw new Error("Invalid handoff: post_pipeline entries require a canonical id");
69
+ }
70
+ }
71
+ const completionTimestamp = new Date().toISOString();
72
+ let closeoutPath;
73
+ const updatedState = await updateStatusAtomically(cwd, async (lockedState) => {
74
+ const phase = resolvePhase(lockedState, validation.phaseId);
75
+ if (!phase?.primary_output)
76
+ throw new Error(`Phase ${validation.phaseId} is missing primary_output.`);
77
+ const nextStatus = normalizeStatus(lockedState.status, lockedState.pipeline, lockedState.status.pipeline, lockedState.cwd);
78
+ const existingPhase = nextStatus.phases[validation.phaseId] ?? {
79
+ status: "pending",
80
+ owner_notes: [],
81
+ outputs_present: [],
82
+ open_questions: [],
83
+ carry_forward: [],
84
+ };
85
+ const gapEntries = validation.rows
86
+ .filter((row) => row.result.toUpperCase().includes("PARTIAL"))
87
+ .map((row) => ({
88
+ kind: "needs-maintainer-decision",
89
+ description: row.criterion || "Partial validation gap",
90
+ deferred_reason: row.evidence || "Marked PARTIAL by validation",
91
+ }));
92
+ autoAssignIds(gapEntries, "oq", validation.phaseId);
93
+ const mergedOpenQuestions = [...existingPhase.open_questions];
94
+ for (const candidate of gapEntries) {
95
+ if (!mergedOpenQuestions.some((entry) => entry.description === candidate.description && entry.deferred_reason === candidate.deferred_reason)) {
96
+ mergedOpenQuestions.push(candidate);
97
+ }
98
+ }
99
+ nextStatus.phases[validation.phaseId] = {
100
+ status: "complete",
101
+ owner_notes: uniqueStrings([
102
+ ...existingPhase.owner_notes,
103
+ `Completed via ${sourceLabel}.`,
104
+ `Primary output: .codecarto/${validation.primaryOutput}`,
105
+ `Validation: ${validation.overall}`,
106
+ ]),
107
+ outputs_present: uniqueStrings([...existingPhase.outputs_present, validation.primaryOutput]),
108
+ open_questions: mergedOpenQuestions,
109
+ carry_forward: existingPhase.carry_forward ?? [],
110
+ };
111
+ if (handoff)
112
+ applyHandoff(nextStatus, handoff);
113
+ nextStatus.last_updated = completionTimestamp;
114
+ const nextWorkspace = { ...lockedState, status: nextStatus };
115
+ const nextEligible = getNextEligiblePhase(nextWorkspace);
116
+ nextStatus.current_phase = nextEligible?.id ?? "complete";
117
+ nextStatus.next_actions = nextEligible
118
+ ? [`Begin ${nextEligible.id} phase by producing ${nextEligible.primary_output ?? `findings/${nextEligible.id}/`}`]
119
+ : ["All phases complete. Review findings, open questions, and downstream implementation notes."];
120
+ closeoutPath = await writeCompletionArtifacts(lockedState.workspaceDir, validation.phaseId, validation, completionTimestamp, handoff);
121
+ return { state: { ...nextWorkspace, status: nextStatus } };
122
+ });
123
+ return {
124
+ updatedState,
125
+ closeoutNotice: closeoutPath ? `Closeout: ${closeoutPath}` : undefined,
126
+ };
127
+ }
@@ -22,6 +22,7 @@ export function renderDashboard(inputs) {
22
22
  renderUsagePanel(inputs),
23
23
  renderActivityTimeline(inputs.usage.runs),
24
24
  renderOpenQuestionsRollup(inputs.status),
25
+ renderPostPipelineWork(inputs.status),
25
26
  renderCloseoutsList(inputs),
26
27
  renderFooter(inputs),
27
28
  `</div>`,
@@ -74,6 +75,7 @@ function renderSidebar(inputs) {
74
75
  `<a class="cc-nav-section" href="#phases">Phases</a>`,
75
76
  phaseLinks,
76
77
  `<a class="cc-nav-section" href="#usage">Usage</a>`,
78
+ (inputs.status.post_pipeline?.length ?? 0) > 0 ? `<a class="cc-nav-section" href="#post-pipeline">Post-pipeline</a>` : "",
77
79
  `<a class="cc-nav-section" href="#closeouts">Closeouts</a>`,
78
80
  `</nav>`,
79
81
  `<button type="button" class="cc-export" data-export>Export dashboard JSON</button>`,
@@ -134,6 +136,7 @@ function renderHealthPanel(inputs) {
134
136
  const issues = collectDashboardIssues(inputs);
135
137
  const openQuestionCount = countOpenQuestions(status);
136
138
  const carryForwardCount = countCarryForward(status);
139
+ const postPipelineCount = (status.post_pipeline ?? []).filter((entry) => entry.status !== "resolved").length;
137
140
  const health = issues.some((i) => i.severity === "blocker") ? "attention required" : issues.length ? "review recommended" : completed === total ? "complete" : "on track";
138
141
  const healthClass = issues.some((i) => i.severity === "blocker") ? "bad" : issues.length ? "warn" : "ok";
139
142
  const tokenText = usageHasTokenAccounting(usage) ? formatTokenCount(totals.tokens.input + totals.tokens.output) : usage.runs.length ? "unavailable" : "0";
@@ -150,6 +153,7 @@ function renderHealthPanel(inputs) {
150
153
  renderHealthMetric("Artifacts needing attention", String(issues.length), issues.length ? "bad" : "ok"),
151
154
  renderHealthMetric("Open questions", String(openQuestionCount), openQuestionCount ? "warn" : "ok"),
152
155
  renderHealthMetric("Carry-forward items", String(carryForwardCount), carryForwardCount ? "warn" : "ok"),
156
+ renderHealthMetric("Post-pipeline work", String(postPipelineCount), postPipelineCount ? "neutral" : "ok"),
153
157
  renderHealthMetric("Tool uses", String(totals.tool_uses), "neutral"),
154
158
  renderHealthMetric("Runtime", formatMillis(totals.duration_ms), "neutral"),
155
159
  renderHealthMetric("Tokens", tokenText, tokenText === "unavailable" ? "warn" : "neutral"),
@@ -369,7 +373,8 @@ function renderPhaseLastRun(run) {
369
373
  const tokensTotal = formatRunTokens(run);
370
374
  const sessionLink = run.session_file ? renderSafeLink(run.session_file, "transcript") : undefined;
371
375
  const session = sessionLink ? `<dt>Session</dt><dd>${sessionLink}</dd>` : "";
372
- return [`<div class="cc-phase-section">`, `<h3>Last run</h3>`, `<dl class="cc-run-meta">`, `<dt>Timestamp</dt><dd>${escapeHtml(run.timestamp)}</dd>`, `<dt>Status</dt><dd>${escapeHtml(run.status)}</dd>`, `<dt>Turns</dt><dd>${run.turn_count}</dd>`, `<dt>Tool uses</dt><dd>${run.tool_uses}</dd>`, `<dt>Tokens</dt><dd>${escapeHtml(tokensTotal)}</dd>`, `<dt>Duration</dt><dd>${escapeHtml(formatMillis(run.duration_ms))}</dd>`, session, `</dl>`, `</div>`].join("");
376
+ const compactions = run.compactions ? `<dt>Compactions</dt><dd>${formatCompactionCounts(run.compactions)}</dd>` : "";
377
+ return [`<div class="cc-phase-section">`, `<h3>Last run</h3>`, `<dl class="cc-run-meta">`, `<dt>Timestamp</dt><dd>${escapeHtml(run.timestamp)}</dd>`, `<dt>Status</dt><dd>${escapeHtml(run.status)}</dd>`, `<dt>Turns</dt><dd>${run.turn_count}</dd>`, `<dt>Tool uses</dt><dd>${run.tool_uses}</dd>`, `<dt>Tokens</dt><dd>${escapeHtml(tokensTotal)}</dd>`, `<dt>Duration</dt><dd>${escapeHtml(formatMillis(run.duration_ms))}</dd>`, compactions, session, `</dl>`, `</div>`].join("");
373
378
  }
374
379
  function renderUsagePanel(inputs) {
375
380
  const { usage, pipeline, status } = inputs;
@@ -382,10 +387,11 @@ function renderUsagePanel(inputs) {
382
387
  const t = perPhase.get(phaseId);
383
388
  if (!t) {
384
389
  const complete = status.phases[phaseId]?.status === "complete";
385
- return `<tr class="${complete ? "cc-usage-missing" : ""}"><td><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a></td><td>0</td><td>—</td><td>—</td><td>—</td><td>${complete ? "usage not recorded" : "not run"}</td></tr>`;
390
+ return `<tr class="${complete ? "cc-usage-missing" : ""}"><td><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a></td><td>0</td><td>—</td><td>—</td><td>—</td><td>—</td><td>${complete ? "usage not recorded" : "not run"}</td></tr>`;
386
391
  }
387
392
  const tokensTotal = t.tokens.input + t.tokens.output;
388
- return `<tr><td><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a></td><td>${t.runs}</td><td>${escapeHtml(tokenAccounting ? formatTokenCount(tokensTotal) : "unavailable")}</td><td>${renderUsageBar(t.tool_uses, maxTools, String(t.tool_uses))}</td><td>${renderUsageBar(t.duration_ms, maxDuration, formatMillis(t.duration_ms))}</td><td>${usagePhaseNote(phaseId, status)}</td></tr>`;
393
+ const compactions = t.compaction_runs > 0 ? formatCompactionTriplet(t.compactions) : "unavailable";
394
+ return `<tr><td><a href="#${phaseAnchor(phaseId)}">${escapeHtml(phaseId)}</a></td><td>${t.runs}</td><td>${escapeHtml(tokenAccounting ? formatTokenCount(tokensTotal) : "unavailable")}</td><td>${renderUsageBar(t.tool_uses, maxTools, String(t.tool_uses))}</td><td>${renderUsageBar(t.duration_ms, maxDuration, formatMillis(t.duration_ms))}</td><td>${compactions}</td><td>${usagePhaseNote(phaseId, status)}</td></tr>`;
389
395
  }).join("");
390
396
  return [
391
397
  `<section class="cc-card cc-usage" id="usage" aria-label="Token usage" data-section data-search-text="usage tokens tool duration">`,
@@ -393,7 +399,7 @@ function renderUsagePanel(inputs) {
393
399
  usage.runs.length === 0 ? `<p class="cc-empty">No phase runs recorded yet.</p>` : `<dl class="cc-usage-totals">${renderUsageTotalsList(totals, tokenAccounting)}</dl>`,
394
400
  renderUsageInsights(perPhase, status),
395
401
  `<table class="cc-usage-table">`,
396
- `<thead><tr><th>Phase</th><th>Runs</th><th>Tokens</th><th>Tools</th><th>Duration</th><th>State</th></tr></thead>`,
402
+ `<thead><tr><th>Phase</th><th>Runs</th><th>Tokens</th><th>Tools</th><th>Duration</th><th>Compactions</th><th>State</th></tr></thead>`,
397
403
  `<tbody>${rows}</tbody>`,
398
404
  `</table>`,
399
405
  `</section>`,
@@ -405,7 +411,18 @@ function renderUsageTotalsList(totals, tokenAccounting) {
405
411
  ? `${escapeHtml(formatTokenCount(totals.tokens.input))} / ${escapeHtml(formatTokenCount(totals.tokens.output))} / ${escapeHtml(formatTokenCount(totals.tokens.cache_write))}`
406
412
  : `<span class="cc-muted">unavailable — host did not report token counts</span>`;
407
413
  const tokenTotal = tokenAccounting ? escapeHtml(formatTokenCount(tokensTotal)) : `<span class="cc-muted">unavailable</span>`;
408
- return [`<dt>Total runs</dt><dd>${totals.runs}</dd>`, `<dt>Tokens (in / out / cache)</dt><dd>${tokenDetail}</dd>`, `<dt>Total tokens</dt><dd>${tokenTotal}</dd>`, `<dt>Tool uses</dt><dd>${totals.tool_uses}</dd>`, `<dt>Total duration</dt><dd>${escapeHtml(formatMillis(totals.duration_ms))}</dd>`].join("");
414
+ const compactionCounts = totals.compaction_runs > 0 ? formatCompactionCounts(totals.compactions) : `<span class="cc-muted">unavailable — host did not report compaction events</span>`;
415
+ const compactionReasons = totals.compaction_runs > 0 ? `<dt>Reasons</dt><dd>${formatCompactionReasons(totals.compactions)}</dd>` : "";
416
+ return [`<dt>Total runs</dt><dd>${totals.runs}</dd>`, `<dt>Tokens (in / out / cache)</dt><dd>${tokenDetail}</dd>`, `<dt>Total tokens</dt><dd>${tokenTotal}</dd>`, `<dt>Tool uses</dt><dd>${totals.tool_uses}</dd>`, `<dt>Total duration</dt><dd>${escapeHtml(formatMillis(totals.duration_ms))}</dd>`, `<dt>Compactions</dt><dd>${compactionCounts}</dd>`, compactionReasons].join("");
417
+ }
418
+ function formatCompactionCounts(value) {
419
+ return `${value.successful} successful · ${value.failed} failed · ${value.aborted} aborted`;
420
+ }
421
+ function formatCompactionReasons(value) {
422
+ return `threshold ${value.reasons.threshold} · overflow ${value.reasons.overflow} · manual ${value.reasons.manual}`;
423
+ }
424
+ function formatCompactionTriplet(value) {
425
+ return `${value.successful} / ${value.failed} / ${value.aborted}`;
409
426
  }
410
427
  function renderUsageInsights(perPhase, status) {
411
428
  if (perPhase.size === 0)
@@ -455,7 +472,7 @@ function renderOpenQuestionsRollup(status) {
455
472
  for (const [phaseId, phaseState] of Object.entries(status.phases)) {
456
473
  const questions = [];
457
474
  for (const q of phaseState.open_questions ?? []) {
458
- const key = `${q.kind ?? ""}|${q.description ?? ""}|${q.deferred_reason ?? ""}`;
475
+ const key = q.id ?? `${q.kind ?? ""}|${q.description ?? ""}|${q.deferred_reason ?? ""}`;
459
476
  if (seen.has(key))
460
477
  continue;
461
478
  seen.add(key);
@@ -473,6 +490,19 @@ function renderOpenQuestionsRollup(status) {
473
490
  const kindSummary = [...byKind.entries()].sort((a, b) => b[1] - a[1]).map(([kind, count]) => `<span class="cc-kind-chip"><strong>${count}</strong>${escapeHtml(kind)}</span>`).join("");
474
491
  return [`<section class="cc-card cc-rollup" aria-label="Open questions roll-up" data-section data-search-text="open questions">`, `<div class="cc-section-head"><h2>Open questions</h2><span>${total} unique</span></div>`, `<div class="cc-kind-summary">${kindSummary}</div>`, buckets.join("\n"), `</section>`].join("\n");
475
492
  }
493
+ function renderPostPipelineWork(status) {
494
+ const items = status.post_pipeline ?? [];
495
+ if (items.length === 0)
496
+ return "";
497
+ const pending = items.filter((entry) => entry.status !== "resolved").length;
498
+ const rows = items.map((entry) => {
499
+ const state = entry.status ?? "pending";
500
+ const source = entry.source_phase ? `<span class="cc-pill cc-pill-target">from ${escapeHtml(entry.source_phase)}</span>` : "";
501
+ const kind = entry.kind ? `<span class="cc-kind">${escapeHtml(String(entry.kind))}</span>` : "";
502
+ return `<li>${kind}<strong>${escapeHtml(entry.id ?? "unidentified")}</strong> ${escapeHtml(entry.description ?? "")}${source}<span class="cc-pill">${escapeHtml(state)}</span></li>`;
503
+ }).join("");
504
+ return [`<section class="cc-card cc-rollup" id="post-pipeline" aria-label="Post-pipeline work" data-section data-search-text="post pipeline spikes amendments deltas decisions reruns">`, `<div class="cc-section-head"><h2>Post-pipeline work</h2><span>${pending} pending · ${items.length} total</span></div>`, `<p class="cc-muted">Optional work after the active pipeline; these items do not make pipeline completion partial.</p>`, `<ul class="cc-question-list">${rows}</ul>`, `</section>`].join("\n");
505
+ }
476
506
  function renderCloseoutsList(inputs) {
477
507
  const closeouts = inputs.closeouts;
478
508
  if (closeouts.length === 0)
@@ -522,7 +552,7 @@ function renderExportData(inputs) {
522
552
  secondary_outputs: outputs?.secondary ?? [],
523
553
  };
524
554
  });
525
- const data = { project: inputs.status.project_name, generatedAt: inputs.generatedAt, packageVersion: inputs.packageVersion, phases, usage: inputs.usage, closeouts: inputs.closeouts };
555
+ const data = { project: inputs.status.project_name, generatedAt: inputs.generatedAt, packageVersion: inputs.packageVersion, phases, post_pipeline: inputs.status.post_pipeline, usage: inputs.usage, closeouts: inputs.closeouts };
526
556
  return `<script id="cc-dashboard-data" type="application/json">${escapeJsonForScript(data)}</script>`;
527
557
  }
528
558
  function renderScripts() {
@@ -5,7 +5,9 @@ export * from "./status.ts";
5
5
  export * from "./pipeline.ts";
6
6
  export * from "./prompts.ts";
7
7
  export * from "./workspace.ts";
8
+ export * from "./completion.ts";
8
9
  export * from "./orchestrator-config.ts";
9
10
  export * from "./usage.ts";
10
11
  export * from "./dashboard.ts";
11
12
  export * from "./library.ts";
13
+ export * from "./synthesis.ts";
@@ -8,7 +8,9 @@ export * from "./status.js";
8
8
  export * from "./pipeline.js";
9
9
  export * from "./prompts.js";
10
10
  export * from "./workspace.js";
11
+ export * from "./completion.js";
11
12
  export * from "./orchestrator-config.js";
12
13
  export * from "./usage.js";
13
14
  export * from "./dashboard.js";
14
15
  export * from "./library.js";
16
+ export * from "./synthesis.js";
@@ -9,6 +9,7 @@ export const PIPELINE_ALIASES = {
9
9
  "defect-scan": "workflow/pipeline-defect-scan.yaml",
10
10
  lite: "workflow/pipeline-lite.yaml",
11
11
  "architecture-only": "workflow/pipeline-architecture-only.yaml",
12
+ synthesis: "workflow/pipeline-synthesis.yaml",
12
13
  };
13
14
  export const DEFAULT_PIPELINE_PATH = "workflow/pipeline-full-with-deep-audit.yaml";
14
15
  export function getPhaseMap(pipeline) {
@@ -1,7 +1,15 @@
1
- import type { CarryForwardEntry, OpenQuestionEntry, PipelinePhase, ValidationResult, WorkspaceState } from "./types.ts";
1
+ import type { CarryForwardEntry, OpenQuestionEntry, PipelinePhase, WorkspaceState } from "./types.ts";
2
+ import { type PhasePreflightResult } from "./synthesis.ts";
2
3
  export declare function describeEntry(entry: OpenQuestionEntry | CarryForwardEntry): string;
3
4
  export declare function collectRoutedCarryForward(state: WorkspaceState, targetPhaseId: string): CarryForwardEntry[];
4
5
  export interface BuildPhasePromptOptions {
6
+ /**
7
+ * A result the caller already validated immediately before prompt building.
8
+ * Pi uses this to preserve its caller-specific preflight error handling
9
+ * without repeating the same filesystem reads. Callers that omit it (MCP
10
+ * and direct/forced prompting) remain self-contained and run preflight here.
11
+ */
12
+ preflight?: PhasePreflightResult;
5
13
  /**
6
14
  * Set when the phase is being run inside `/codecarto-next --auto` (or any
7
15
  * other non-interactive driver). Suppresses interactive hooks that would
@@ -14,7 +22,5 @@ export interface BuildPhasePromptOptions {
14
22
  }
15
23
  export declare function buildPhasePrompt(state: WorkspaceState, phase: PipelinePhase, forced: boolean, options?: BuildPhasePromptOptions): Promise<string>;
16
24
  export declare function closeoutFileName(date: string, phaseOrModule: string): string;
17
- export declare function buildThreadLogEntry(phaseOrModule: string, validation: ValidationResult, timestamp: string): string;
18
- export declare function ensureCloseoutStub(workspaceDir: string, phaseOrModule: string, timestamp: string): Promise<string | null>;
19
25
  export declare function listSkillNames(workspaceDir: string): Promise<string[]>;
20
26
  export declare function buildSkillPrompt(state: WorkspaceState, skillName: string): Promise<string>;