codecartographer-pi 0.11.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 (39) hide show
  1. package/.codecarto/GUIDE.md +30 -15
  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/spec-merge/README.md +3 -0
  7. package/.codecarto/findings/spec-merge/SKILL.md +23 -0
  8. package/.codecarto/findings/vision-capture/README.md +3 -0
  9. package/.codecarto/findings/vision-capture/SKILL.md +26 -0
  10. package/.codecarto/inputs/vision.md +11 -0
  11. package/.codecarto/templates/merged-spec.md +58 -0
  12. package/.codecarto/templates/phase-handoff.yaml +22 -0
  13. package/.codecarto/templates/project-plan.md +70 -0
  14. package/.codecarto/templates/proposal.md +43 -0
  15. package/.codecarto/templates/vision.md +63 -0
  16. package/.codecarto/workflow/pipeline-synthesis.yaml +105 -0
  17. package/.codecarto/workflow/status.yaml +2 -0
  18. package/README.md +52 -6
  19. package/dist/core/completion.d.ts +6 -0
  20. package/dist/core/completion.js +127 -0
  21. package/dist/core/dashboard.js +19 -2
  22. package/dist/core/index.d.ts +2 -0
  23. package/dist/core/index.js +2 -0
  24. package/dist/core/pipeline.js +1 -0
  25. package/dist/core/prompts.d.ts +9 -3
  26. package/dist/core/prompts.js +37 -26
  27. package/dist/core/status.d.ts +7 -1
  28. package/dist/core/status.js +187 -2
  29. package/dist/core/synthesis.d.ts +31 -0
  30. package/dist/core/synthesis.js +140 -0
  31. package/dist/core/types.d.ts +29 -1
  32. package/dist/core/workspace.d.ts +3 -1
  33. package/dist/core/workspace.js +39 -3
  34. package/dist/core/yaml.js +24 -0
  35. package/dist/extensions/codecarto/auto-runner.d.ts +3 -1
  36. package/dist/extensions/codecarto/auto-runner.js +20 -68
  37. package/dist/extensions/codecarto/index.js +125 -11
  38. package/dist/mcp-server/server.js +24 -68
  39. package/package.json +3 -2
@@ -17,7 +17,7 @@ import { buildSteeringMessage, rewritePhasePrompt } from "./agent-rewriter.js";
17
17
  import { buildPhaseSummary } from "./agent-summary.js";
18
18
  import { getAgentsWidget } from "./agent-widget.js";
19
19
  import { writeDashboard } from "./dashboard-writer.js";
20
- import { appendUsageRun, buildPhasePrompt, buildThreadLogEntry, buildValidationSummary, closeoutFileName, dateOnly, ensureCloseoutStub, formatMillis, formatTokenCount, getNextEligiblePhase, getWorkspaceState, loadCodecartoConfig, normalizeStatus, PACKAGE_VERSION, resolvePhase, uniqueStrings, updateStatusAtomically, validatePhaseOutput, } from "../../core/index.js";
20
+ import { appendUsageRun, buildPhasePrompt, buildValidationSummary, completeValidatedPhase, formatMillis, formatTokenCount, getNextEligiblePhase, getWorkspaceState, loadCodecartoConfig, PACKAGE_VERSION, PhasePreflightError, runPhasePreflight, validatePhaseOutput, } from "../../core/index.js";
21
21
  /**
22
22
  * Run one phase end to end: optional LLM-steered rewrite, spawn the sub-agent,
23
23
  * wait for it, then emit the side effects the historical /codecarto-next chain
@@ -29,7 +29,10 @@ import { appendUsageRun, buildPhasePrompt, buildThreadLogEntry, buildValidationS
29
29
  * getPhaseActivity) and attached the agents widget if it wanted live progress.
30
30
  */
31
31
  export async function runSinglePhase(ctx, pi, state, phase, options) {
32
- let prompt = await buildPhasePrompt(state, phase, false, { auto: options.auto === true });
32
+ let prompt = await buildPhasePrompt(state, phase, false, {
33
+ auto: options.auto === true,
34
+ preflight: options.preflight,
35
+ });
33
36
  if (options.llmSteerEnabled) {
34
37
  if (ctx.hasUI)
35
38
  ctx.ui.notify(`Customizing ${phase.id} prompt via LLM rewriter…`, "info");
@@ -149,73 +152,9 @@ export function isPhaseRunning(phaseId) {
149
152
  return existing?.status === "running";
150
153
  }
151
154
  export async function autoCompletePhase(ctx, validation) {
152
- const completionTimestamp = new Date().toISOString();
153
- const updatedState = await updateStatusAtomically(ctx.cwd, (lockedState) => {
154
- const phase = resolvePhase(lockedState, validation.phaseId);
155
- if (!phase?.primary_output) {
156
- throw new Error(`Phase ${validation.phaseId} is missing primary_output.`);
157
- }
158
- const nextStatus = normalizeStatus(lockedState.status, lockedState.pipeline, lockedState.status.pipeline, lockedState.cwd);
159
- const existingPhase = nextStatus.phases[validation.phaseId] ?? {
160
- status: "pending",
161
- owner_notes: [],
162
- outputs_present: [],
163
- open_questions: [],
164
- carry_forward: [],
165
- };
166
- const gapEntries = validation.rows
167
- .filter((row) => row.result.toUpperCase().includes("PARTIAL"))
168
- .map((row) => ({
169
- kind: "needs-maintainer-decision",
170
- description: row.criterion || "Partial validation gap",
171
- deferred_reason: row.evidence || "Marked PARTIAL by validation",
172
- }));
173
- const mergedOpenQuestions = [...existingPhase.open_questions];
174
- for (const candidate of gapEntries) {
175
- const dupe = mergedOpenQuestions.some((entry) => entry.description === candidate.description && entry.deferred_reason === candidate.deferred_reason);
176
- if (!dupe)
177
- mergedOpenQuestions.push(candidate);
178
- }
179
- nextStatus.phases[validation.phaseId] = {
180
- status: "complete",
181
- owner_notes: uniqueStrings([
182
- ...existingPhase.owner_notes,
183
- `Completed via /codecarto-complete on ${completionTimestamp}.`,
184
- `Primary output: .codecarto/${validation.primaryOutput}`,
185
- `Validation: ${validation.overall}`,
186
- ]).slice(-3),
187
- outputs_present: uniqueStrings([...existingPhase.outputs_present, validation.primaryOutput]),
188
- open_questions: mergedOpenQuestions,
189
- carry_forward: existingPhase.carry_forward ?? [],
190
- };
191
- nextStatus.last_updated = completionTimestamp;
192
- const updatedWorkspaceState = {
193
- ...lockedState,
194
- status: nextStatus,
195
- };
196
- const nextEligible = getNextEligiblePhase(updatedWorkspaceState);
197
- nextStatus.current_phase = nextEligible?.id ?? "complete";
198
- nextStatus.next_actions = nextEligible
199
- ? [`Begin ${nextEligible.id} phase by producing ${nextEligible.primary_output ?? `findings/${nextEligible.id}/`}`]
200
- : ["All phases complete. Review findings, open questions, and downstream implementation notes."];
201
- return {
202
- state: { ...updatedWorkspaceState, status: nextStatus },
203
- threadLogEntry: buildThreadLogEntry(validation.phaseId, validation, completionTimestamp),
204
- };
205
- });
206
- let closeoutNotice;
207
- try {
208
- const created = await ensureCloseoutStub(updatedState.workspaceDir, validation.phaseId, completionTimestamp);
209
- if (created) {
210
- closeoutNotice = `Closeout stub: .codecarto/closeouts/${closeoutFileName(dateOnly(completionTimestamp), validation.phaseId)} (fill it in)`;
211
- }
212
- }
213
- catch (error) {
214
- const message = error instanceof Error ? error.message : String(error);
215
- closeoutNotice = `Closeout stub not created: ${message}`;
216
- }
155
+ const result = await completeValidatedPhase(ctx.cwd, validation, "/codecarto-complete");
217
156
  void writeDashboard(ctx.cwd, PACKAGE_VERSION);
218
- return { updatedState, closeoutNotice };
157
+ return result;
219
158
  }
220
159
  export function decideAfterPhase(phaseStatus, phaseError, validation, strict) {
221
160
  if (phaseStatus === "aborted")
@@ -266,6 +205,18 @@ export async function runAuto(ctx, pi, initialState, options) {
266
205
  reason: "Pipeline complete.",
267
206
  });
268
207
  }
208
+ let preflight;
209
+ try {
210
+ preflight = await runPhasePreflight(state, phase);
211
+ }
212
+ catch (err) {
213
+ const message = err instanceof Error ? err.message : String(err);
214
+ return finish({
215
+ outcome: "stopped",
216
+ reason: message,
217
+ stoppedAt: { phaseId: phase.id, error: err instanceof PhasePreflightError ? undefined : message },
218
+ });
219
+ }
269
220
  if (isPhaseRunning(phase.id)) {
270
221
  return finish({
271
222
  outcome: "stopped",
@@ -277,6 +228,7 @@ export async function runAuto(ctx, pi, initialState, options) {
277
228
  llmSteerEnabled,
278
229
  signal: options.signal,
279
230
  auto: true,
231
+ preflight,
280
232
  });
281
233
  // Accumulate tokens whether the phase succeeded, was aborted, or errored.
282
234
  totalTokens.input += phaseResult.activity.lifetimeUsage.input;
@@ -1,4 +1,4 @@
1
- import { cp, mkdir, rm, writeFile } from "node:fs/promises";
1
+ import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
2
  import { basename, join, resolve } from "node:path";
3
3
  import { autoCompletePhase, buildAutoSummary, isPhaseRunning, runAuto, runSinglePhase } from "./auto-runner.js";
4
4
  import { disposeAgentsWidget } from "./agent-widget.js";
@@ -7,10 +7,31 @@ import { narrateDashboard } from "./dashboard-narrator.js";
7
7
  import { writeDashboard } from "./dashboard-writer.js";
8
8
  import { parseNextFlags } from "./next-flags.js";
9
9
  import { phaseCompactionExtension } from "./phase-compaction.js";
10
- import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPath, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, PIPELINE_ALIASES, resolvePhase, resolvePipelineChoice, stringifySimpleYaml, validatePhaseOutput, } from "../../core/index.js";
10
+ import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPath, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, runPhasePreflight, stringifySimpleYaml, validatePhaseOutput, } from "../../core/index.js";
11
11
  const STATUS_WIDGET_ID = "codecarto-widget";
12
12
  const STATUS_LINE_ID = "codecarto-status";
13
13
  const SAFE_TOOL_NAMES = ["read", "grep", "find", "ls", "edit", "write"];
14
+ function derivePublishHeadline(spec, cwd) {
15
+ const summary = spec.split(/^##\s+System Summary\s*$/mi)[1]?.split(/^##\s+/m)[0] ?? "";
16
+ const candidate = summary
17
+ .split(/\r?\n/)
18
+ .map((line) => line.trim())
19
+ .find((line) => line && !line.startsWith("<!--") && !line.startsWith("-->") && !line.startsWith("#"));
20
+ return candidate?.replace(/\s+/g, " ").slice(0, 280) || `Reimplementation specification for ${basename(cwd)}.`;
21
+ }
22
+ function piGeneration(ctx) {
23
+ return {
24
+ surface: "pi-extension",
25
+ agent: "pi",
26
+ agent_version: "unknown",
27
+ model: ctx.model?.id ?? "unknown",
28
+ model_vendor: ctx.model?.provider ?? "unknown",
29
+ // Pi's extension context exposes the selected model but not the active
30
+ // thinking level. Preserve that uncertainty instead of inferring it.
31
+ reasoning: "unknown",
32
+ notes: "",
33
+ };
34
+ }
14
35
  function formatUsageTokens(count) {
15
36
  if (count >= 1_000_000)
16
37
  return `${(count / 1_000_000).toFixed(2)}M`;
@@ -32,16 +53,19 @@ function buildStatusLines(state, extraLines = []) {
32
53
  const currentPhase = nextPhase?.id ?? state.status.current_phase ?? "complete";
33
54
  const pipelineLabel = getPipelineLabel(state.status.pipeline);
34
55
  const completedCount = state.pipeline.phase_order.filter((phaseId) => state.status.phases[phaseId]?.status === "complete").length;
35
- const currentOpenQuestions = currentPhase === "complete" ? 0 : state.status.phases[currentPhase]?.open_questions.length ?? 0;
56
+ const terminalOpenQuestions = Object.values(state.status.phases).reduce((sum, phase) => sum + (phase.open_questions?.length ?? 0), 0);
36
57
  const totalCarryForward = Object.values(state.status.phases).reduce((sum, phase) => sum + (phase.carry_forward?.length ?? 0), 0);
58
+ const postPipelinePending = state.status.post_pipeline.filter((entry) => entry.status !== "resolved").length;
37
59
  const nextAction = state.status.next_actions[0] ?? (nextPhase ? `Next: ${nextPhase.id}` : "All phases complete.");
38
60
  const lines = [
39
61
  "CodeCartographer",
40
62
  `Phase: ${currentPhase}`,
63
+ `Pipeline state: ${currentPhase === "complete" ? "complete" : "in progress"}`,
41
64
  `Pipeline: ${pipelineLabel}`,
42
65
  `Progress: ${completedCount}/${state.pipeline.phase_order.length} complete`,
43
- `Open questions: ${currentOpenQuestions}`,
44
- `Carry-forward: ${totalCarryForward}`,
66
+ `Open questions (terminal unresolved): ${terminalOpenQuestions}`,
67
+ `Carry-forward (pipeline phases): ${totalCarryForward}`,
68
+ `Post-pipeline work: ${postPipelinePending} pending`,
45
69
  `Next: ${nextAction}`,
46
70
  ];
47
71
  if (extraLines.length > 0) {
@@ -135,12 +159,16 @@ export default function codeCartographerExtension(pi) {
135
159
  const inputPath = typeof event.input.path === "string" ? event.input.path : "";
136
160
  const strippedPath = inputPath.startsWith("@") ? inputPath.slice(1) : inputPath;
137
161
  const targetPath = await canonicalPath(resolve(ctx.cwd, strippedPath));
138
- const allowedRoot = await canonicalPath(workspaceDir);
139
- if (!isWithinPath(targetPath, allowedRoot)) {
162
+ const allowedRoots = [await canonicalPath(workspaceDir)];
163
+ const config = await loadCodecartoConfig(workspaceDir);
164
+ if (config.library.path && await discoverLibrary(config.library.path)) {
165
+ allowedRoots.push(await canonicalPath(config.library.path));
166
+ }
167
+ if (!allowedRoots.some((allowedRoot) => isWithinPath(targetPath, allowedRoot))) {
140
168
  if (ctx.hasUI) {
141
- ctx.ui.notify(`Blocked ${event.toolName} outside .codecarto/: ${inputPath}`, "warning");
169
+ ctx.ui.notify(`Blocked ${event.toolName} outside .codecarto/ or configured library: ${inputPath}`, "warning");
142
170
  }
143
- return { block: true, reason: `CodeCartographer mode only allows ${event.toolName} within .codecarto/` };
171
+ return { block: true, reason: `CodeCartographer mode only allows ${event.toolName} within .codecarto/ or the configured CodeCartographer library.` };
144
172
  }
145
173
  }
146
174
  return undefined;
@@ -292,6 +320,17 @@ export default function codeCartographerExtension(pi) {
292
320
  ctx.ui.notify("All CodeCartographer phases are complete.", "info");
293
321
  return;
294
322
  }
323
+ let preflight;
324
+ try {
325
+ preflight = await runPhasePreflight(state, phase);
326
+ }
327
+ catch (error) {
328
+ const message = error instanceof Error ? error.message : String(error);
329
+ lastFeedbackLines = [message];
330
+ setUiState(ctx, state, lastFeedbackLines);
331
+ ctx.ui.notify(message, error instanceof PhasePreflightError ? "warning" : "error");
332
+ return;
333
+ }
295
334
  // Reject re-entry: don't spawn a duplicate runner for a phase that's
296
335
  // already in flight from a previous /codecarto-next invocation.
297
336
  if (isPhaseRunning(phase.id)) {
@@ -305,7 +344,7 @@ export default function codeCartographerExtension(pi) {
305
344
  // Fire-and-forget: keep the TUI responsive while the sub-agent works.
306
345
  // runSinglePhase handles all side effects (steering message, notify,
307
346
  // phase summary, recordUsage, dashboard regen, clearPhase linger).
308
- void runSinglePhase(ctx, pi, state, phase, { llmSteerEnabled, signal: ctx.signal })
347
+ void runSinglePhase(ctx, pi, state, phase, { llmSteerEnabled, signal: ctx.signal, preflight })
309
348
  .finally(() => {
310
349
  // Refresh the status widget after the phase resolves so the
311
350
  // "Open questions / Carry-forward / Next" lines reflect any
@@ -330,7 +369,15 @@ export default function codeCartographerExtension(pi) {
330
369
  ctx.ui.notify(`Unknown phase: ${phaseId}`, "error");
331
370
  return;
332
371
  }
333
- const prompt = await buildPhasePrompt(state, phase, true);
372
+ let prompt;
373
+ try {
374
+ prompt = await buildPhasePrompt(state, phase, true);
375
+ }
376
+ catch (error) {
377
+ const message = error instanceof Error ? error.message : String(error);
378
+ ctx.ui.notify(message, error instanceof PhasePreflightError ? "warning" : "error");
379
+ return;
380
+ }
334
381
  if (ctx.isIdle()) {
335
382
  pi.sendUserMessage(prompt);
336
383
  }
@@ -431,6 +478,73 @@ export default function codeCartographerExtension(pi) {
431
478
  ctx.ui.notify(`Queued CodeCartographer skill: ${skillName}`, "info");
432
479
  },
433
480
  });
481
+ pi.registerCommand("codecarto-publish", {
482
+ description: "Publish the completed reimplementation spec to the configured CodeCartographer library",
483
+ handler: async (_args, ctx) => {
484
+ const state = await ensureWorkspaceState(ctx);
485
+ if (!state)
486
+ return;
487
+ const config = await loadCodecartoConfig(state.workspaceDir);
488
+ if (!config.library.path) {
489
+ ctx.ui.notify("No library.path is configured. Set it in ~/.codecarto/config.yaml or .codecarto/workflow/config.yaml.", "error");
490
+ return;
491
+ }
492
+ const marker = await discoverLibrary(config.library.path);
493
+ if (!marker) {
494
+ ctx.ui.notify(`No CodeCartographer library at ${config.library.path} (missing .codecarto-library).`, "error");
495
+ return;
496
+ }
497
+ const phase = resolvePhase(state, "reimplementation-spec");
498
+ if (!phase?.primary_output) {
499
+ ctx.ui.notify("The active pipeline does not produce a reimplementation spec to publish.", "error");
500
+ return;
501
+ }
502
+ const specPath = join(state.workspaceDir, phase.primary_output);
503
+ if (!(await pathExists(specPath))) {
504
+ ctx.ui.notify(`Reimplementation spec is missing: .codecarto/${phase.primary_output}`, "error");
505
+ return;
506
+ }
507
+ const spec = await readFile(specPath, "utf8");
508
+ const slug = deriveSlug(ctx.cwd);
509
+ const headline = derivePublishHeadline(spec, ctx.cwd);
510
+ const namespace = marker.namespaced ? config.library.namespace ?? undefined : undefined;
511
+ if (marker.namespaced && !namespace) {
512
+ ctx.ui.notify("The configured library is namespaced; set library.namespace before publishing.", "error");
513
+ return;
514
+ }
515
+ const preview = [
516
+ `Publish ${namespace ? `${namespace}/` : ""}${slug} to ${config.library.path}`,
517
+ `Source: ${ctx.cwd}`,
518
+ `Spec: .codecarto/${phase.primary_output}`,
519
+ `Headline: ${headline}`,
520
+ `Provenance: Pi / ${ctx.model?.provider ?? "unknown"} / ${ctx.model?.id ?? "unknown"}`,
521
+ ].join("\n");
522
+ if (config.library.publish_confirm && !(await ctx.ui.confirm("Publish reimplementation spec", preview)))
523
+ return;
524
+ try {
525
+ const result = await publishEntry(config.library.path, spec, {
526
+ slug,
527
+ namespace,
528
+ source_repo: ctx.cwd,
529
+ analyzed_at: new Date().toISOString(),
530
+ pipeline: state.status.pipeline,
531
+ codecarto_version: PACKAGE_VERSION,
532
+ headline,
533
+ tags: [],
534
+ capabilities: [],
535
+ generation: piGeneration(ctx),
536
+ });
537
+ lastFeedbackLines = [`Published ${result.namespace ? `${result.namespace}/` : ""}${result.slug} v${result.version}`, result.isNewVersion ? "New content version." : "Metadata-only update (content unchanged)."];
538
+ await writeDashboard(ctx.cwd, PACKAGE_VERSION);
539
+ await refreshWorkspaceUi(ctx, lastFeedbackLines);
540
+ ctx.ui.notify(`Published ${result.namespace ? `${result.namespace}/` : ""}${result.slug} v${result.version}.`, "info");
541
+ }
542
+ catch (error) {
543
+ const message = error instanceof Error ? error.message : String(error);
544
+ ctx.ui.notify(`Unable to publish: ${message}`, "error");
545
+ }
546
+ },
547
+ });
434
548
  pi.registerCommand("codecarto-usage", {
435
549
  description: "Show cumulative + per-phase token usage from local phase runs",
436
550
  handler: async (_args, ctx) => {
@@ -15,7 +15,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
15
15
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
16
16
  import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
17
17
  import { basename, isAbsolute, join } from "node:path";
18
- import { buildPhasePrompt, buildSkillPrompt, buildThreadLogEntry, buildValidationSummary, canonicalPath, closeoutFileName, createEmptyStatus, dateOnly, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, ensureCloseoutStub, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, listEntries, listSkillNames, loadCodecartoConfig, loadYamlFile, normalizeForComparison, normalizeStatus, PACKAGE_VERSION, packagedWorkspaceDir, pathExists, publishEntry, reindex as libraryReindex, resolvePhase, resolvePipelineChoice, stringifySimpleYaml, uniqueStrings, updateStatusAtomically, validatePhaseOutput, } from "../core/index.js";
18
+ import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, completeValidatedPhase, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, listEntries, listSkillNames, loadCodecartoConfig, loadYamlFile, normalizeForComparison, PACKAGE_VERSION, packagedWorkspaceDir, pathExists, PhasePreflightError, publishEntry, reindex as libraryReindex, resolvePhase, resolvePipelineChoice, stringifySimpleYaml, validatePhaseOutput, } from "../core/index.js";
19
19
  // ---------- input helpers ----------
20
20
  async function validateCwd(cwd) {
21
21
  if (typeof cwd !== "string" || !cwd.trim()) {
@@ -46,6 +46,17 @@ function textResult(text, structured) {
46
46
  result.structuredContent = structured;
47
47
  return result;
48
48
  }
49
+ async function buildMcpPhasePrompt(state, phase, forced) {
50
+ try {
51
+ return await buildPhasePrompt(state, phase, forced);
52
+ }
53
+ catch (error) {
54
+ if (error instanceof PhasePreflightError) {
55
+ throw new McpError(ErrorCode.InvalidRequest, error.message);
56
+ }
57
+ throw error;
58
+ }
59
+ }
49
60
  // ---------- handlers ----------
50
61
  export async function handleInit(args) {
51
62
  const cwd = await validateCwd(args.cwd);
@@ -101,12 +112,16 @@ export async function handleStatus(args) {
101
112
  const completed = state.pipeline.phase_order.filter((id) => state.status.phases[id]?.status === "complete").length;
102
113
  const totalCarryForward = Object.values(state.status.phases).reduce((sum, phase) => sum + (phase.carry_forward?.length ?? 0), 0);
103
114
  const currentOpenQuestions = currentPhase === "complete" ? 0 : state.status.phases[currentPhase]?.open_questions.length ?? 0;
115
+ const terminalOpenQuestions = Object.values(state.status.phases).reduce((sum, phase) => sum + (phase.open_questions?.length ?? 0), 0);
116
+ const postPipelinePending = state.status.post_pipeline.filter((entry) => entry.status !== "resolved").length;
104
117
  const summary = [
105
118
  `Phase: ${currentPhase}`,
119
+ `Pipeline state: ${currentPhase === "complete" ? "complete" : "in progress"}`,
106
120
  `Pipeline: ${getPipelineLabel(state.status.pipeline)} (${state.status.pipeline})`,
107
121
  `Progress: ${completed}/${state.pipeline.phase_order.length} complete`,
108
- `Open questions (current phase): ${currentOpenQuestions}`,
109
- `Carry-forward (all phases): ${totalCarryForward}`,
122
+ `Open questions (terminal unresolved): ${terminalOpenQuestions}`,
123
+ `Carry-forward (pipeline phases): ${totalCarryForward}`,
124
+ `Post-pipeline work: ${postPipelinePending} pending`,
110
125
  `Next: ${state.status.next_actions[0] ?? (nextPhase ? `Begin ${nextPhase.id}` : "All phases complete.")}`,
111
126
  ].join("\n");
112
127
  return textResult(summary, {
@@ -116,7 +131,9 @@ export async function handleStatus(args) {
116
131
  completed,
117
132
  total: state.pipeline.phase_order.length,
118
133
  openQuestionsCurrentPhase: currentOpenQuestions,
134
+ openQuestionsTerminal: terminalOpenQuestions,
119
135
  carryForwardTotal: totalCarryForward,
136
+ postPipelinePending,
120
137
  nextActions: state.status.next_actions,
121
138
  });
122
139
  }
@@ -129,7 +146,7 @@ export async function handleNext(args) {
129
146
  complete: true,
130
147
  });
131
148
  }
132
- const prompt = await buildPhasePrompt(state, phase, false);
149
+ const prompt = await buildMcpPhasePrompt(state, phase, false);
133
150
  return textResult(prompt, { phase: phase.id, forced: false });
134
151
  }
135
152
  export async function handlePhase(args) {
@@ -142,7 +159,7 @@ export async function handlePhase(args) {
142
159
  if (!phase) {
143
160
  throw new McpError(ErrorCode.InvalidParams, `Unknown phase: ${args.phase}`);
144
161
  }
145
- const prompt = await buildPhasePrompt(state, phase, true);
162
+ const prompt = await buildMcpPhasePrompt(state, phase, true);
146
163
  return textResult(prompt, { phase: phase.id, forced: true });
147
164
  }
148
165
  export async function handleValidate(args) {
@@ -172,70 +189,9 @@ export async function handleComplete(args) {
172
189
  if (validation.overall === "FAIL" || validation.overall === "MISSING") {
173
190
  throw new McpError(ErrorCode.InvalidRequest, `Cannot complete ${validation.phaseId}: validation is ${validation.overall}.\n${buildValidationSummary(validation).join("\n")}`);
174
191
  }
175
- const completionTimestamp = new Date().toISOString();
176
- const updatedState = await updateStatusAtomically(cwd, (lockedState) => {
177
- const phase = resolvePhase(lockedState, validation.phaseId);
178
- if (!phase?.primary_output) {
179
- throw new Error(`Phase ${validation.phaseId} is missing primary_output.`);
180
- }
181
- const nextStatus = normalizeStatus(lockedState.status, lockedState.pipeline, lockedState.status.pipeline, lockedState.cwd);
182
- const existingPhase = nextStatus.phases[validation.phaseId] ?? {
183
- status: "pending",
184
- owner_notes: [],
185
- outputs_present: [],
186
- open_questions: [],
187
- carry_forward: [],
188
- };
189
- const gapEntries = validation.rows
190
- .filter((row) => row.result.toUpperCase().includes("PARTIAL"))
191
- .map((row) => ({
192
- kind: "needs-maintainer-decision",
193
- description: row.criterion || "Partial validation gap",
194
- deferred_reason: row.evidence || "Marked PARTIAL by validation",
195
- }));
196
- const mergedOpenQuestions = [...existingPhase.open_questions];
197
- for (const candidate of gapEntries) {
198
- const dupe = mergedOpenQuestions.some((entry) => entry.description === candidate.description && entry.deferred_reason === candidate.deferred_reason);
199
- if (!dupe)
200
- mergedOpenQuestions.push(candidate);
201
- }
202
- nextStatus.phases[validation.phaseId] = {
203
- status: "complete",
204
- owner_notes: uniqueStrings([
205
- ...existingPhase.owner_notes,
206
- `Completed via codecarto_complete on ${completionTimestamp}.`,
207
- `Primary output: .codecarto/${validation.primaryOutput}`,
208
- `Validation: ${validation.overall}`,
209
- ]).slice(-3),
210
- outputs_present: uniqueStrings([...existingPhase.outputs_present, validation.primaryOutput]),
211
- open_questions: mergedOpenQuestions,
212
- carry_forward: existingPhase.carry_forward ?? [],
213
- };
214
- nextStatus.last_updated = completionTimestamp;
215
- const updatedWorkspaceState = {
216
- ...lockedState,
217
- status: nextStatus,
218
- };
219
- const nextEligible = getNextEligiblePhase(updatedWorkspaceState);
220
- nextStatus.current_phase = nextEligible?.id ?? "complete";
221
- nextStatus.next_actions = nextEligible
222
- ? [`Begin ${nextEligible.id} phase by producing ${nextEligible.primary_output ?? `findings/${nextEligible.id}/`}`]
223
- : ["All phases complete. Review findings, open questions, and downstream implementation notes."];
224
- return {
225
- state: { ...updatedWorkspaceState, status: nextStatus },
226
- threadLogEntry: buildThreadLogEntry(validation.phaseId, validation, completionTimestamp),
227
- };
192
+ const { updatedState, closeoutNotice } = await completeValidatedPhase(cwd, validation, "codecarto_complete").catch((error) => {
193
+ throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error));
228
194
  });
229
- let closeoutNotice;
230
- try {
231
- const created = await ensureCloseoutStub(updatedState.workspaceDir, validation.phaseId, completionTimestamp);
232
- if (created) {
233
- closeoutNotice = `Closeout stub created: .codecarto/closeouts/${closeoutFileName(dateOnly(completionTimestamp), validation.phaseId)} (fill it in)`;
234
- }
235
- }
236
- catch (error) {
237
- closeoutNotice = `Closeout stub not created: ${error instanceof Error ? error.message : String(error)}`;
238
- }
239
195
  const lines = [
240
196
  `Marked ${validation.phaseId} complete (validation: ${validation.overall}).`,
241
197
  `Next phase: ${updatedState.status.current_phase}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecartographer-pi",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "CodeCartographer packaged for Pi as an extension-driven workflow wrapper.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -37,7 +37,8 @@
37
37
  "build": "tsc",
38
38
  "prepublishOnly": "npm run build",
39
39
  "test": "node --experimental-strip-types --disable-warning=ExperimentalWarning --test tests/*.test.mjs",
40
- "smoke": "node scripts/smoke-mcp.mjs"
40
+ "smoke": "node scripts/smoke-mcp.mjs",
41
+ "demo:synthesis": "npm run build && node scripts/create-synthesis-demo.mjs"
41
42
  },
42
43
  "dependencies": {
43
44
  "@modelcontextprotocol/sdk": "^1.29.0"