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
@@ -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");
@@ -69,7 +72,16 @@ export async function runSinglePhase(ctx, pi, state, phase, options) {
69
72
  activity.lifetimeUsage.output += usage.output;
70
73
  activity.lifetimeUsage.cacheWrite += usage.cacheWrite;
71
74
  },
72
- }, { sessionName: `CodeCartographer phase: ${phase.id}` }, options.signal);
75
+ onCompactionEnd: (event) => {
76
+ activity.compactions.reasons[event.reason]++;
77
+ if (event.aborted)
78
+ activity.compactions.aborted++;
79
+ else if (event.successful)
80
+ activity.compactions.successful++;
81
+ else
82
+ activity.compactions.failed++;
83
+ },
84
+ }, { sessionName: `CodeCartographer phase: ${phase.id}`, primaryOutput: phase.primary_output }, options.signal);
73
85
  const status = result.aborted ? "aborted" : "completed";
74
86
  finishPhase(phase.id, { status });
75
87
  if (ctx.hasUI) {
@@ -85,6 +97,7 @@ export async function runSinglePhase(ctx, pi, state, phase, options) {
85
97
  turnCount: activity.turnCount,
86
98
  toolUses: activity.toolUses,
87
99
  tokens: activity.lifetimeUsage,
100
+ compactions: activity.compactions,
88
101
  durationMs: (activity.completedAt ?? Date.now()) - activity.startedAt,
89
102
  responseText: result.responseText,
90
103
  sessionFile: result.sessionFile,
@@ -113,6 +126,7 @@ export async function runSinglePhase(ctx, pi, state, phase, options) {
113
126
  turnCount: activity.turnCount,
114
127
  toolUses: activity.toolUses,
115
128
  tokens: activity.lifetimeUsage,
129
+ compactions: activity.compactions,
116
130
  durationMs: (activity.completedAt ?? Date.now()) - activity.startedAt,
117
131
  responseText: "",
118
132
  error: message,
@@ -138,73 +152,9 @@ export function isPhaseRunning(phaseId) {
138
152
  return existing?.status === "running";
139
153
  }
140
154
  export async function autoCompletePhase(ctx, validation) {
141
- const completionTimestamp = new Date().toISOString();
142
- const updatedState = await updateStatusAtomically(ctx.cwd, (lockedState) => {
143
- const phase = resolvePhase(lockedState, validation.phaseId);
144
- if (!phase?.primary_output) {
145
- throw new Error(`Phase ${validation.phaseId} is missing primary_output.`);
146
- }
147
- const nextStatus = normalizeStatus(lockedState.status, lockedState.pipeline, lockedState.status.pipeline, lockedState.cwd);
148
- const existingPhase = nextStatus.phases[validation.phaseId] ?? {
149
- status: "pending",
150
- owner_notes: [],
151
- outputs_present: [],
152
- open_questions: [],
153
- carry_forward: [],
154
- };
155
- const gapEntries = validation.rows
156
- .filter((row) => row.result.toUpperCase().includes("PARTIAL"))
157
- .map((row) => ({
158
- kind: "needs-maintainer-decision",
159
- description: row.criterion || "Partial validation gap",
160
- deferred_reason: row.evidence || "Marked PARTIAL by validation",
161
- }));
162
- const mergedOpenQuestions = [...existingPhase.open_questions];
163
- for (const candidate of gapEntries) {
164
- const dupe = mergedOpenQuestions.some((entry) => entry.description === candidate.description && entry.deferred_reason === candidate.deferred_reason);
165
- if (!dupe)
166
- mergedOpenQuestions.push(candidate);
167
- }
168
- nextStatus.phases[validation.phaseId] = {
169
- status: "complete",
170
- owner_notes: uniqueStrings([
171
- ...existingPhase.owner_notes,
172
- `Completed via /codecarto-complete on ${completionTimestamp}.`,
173
- `Primary output: .codecarto/${validation.primaryOutput}`,
174
- `Validation: ${validation.overall}`,
175
- ]).slice(-3),
176
- outputs_present: uniqueStrings([...existingPhase.outputs_present, validation.primaryOutput]),
177
- open_questions: mergedOpenQuestions,
178
- carry_forward: existingPhase.carry_forward ?? [],
179
- };
180
- nextStatus.last_updated = completionTimestamp;
181
- const updatedWorkspaceState = {
182
- ...lockedState,
183
- status: nextStatus,
184
- };
185
- const nextEligible = getNextEligiblePhase(updatedWorkspaceState);
186
- nextStatus.current_phase = nextEligible?.id ?? "complete";
187
- nextStatus.next_actions = nextEligible
188
- ? [`Begin ${nextEligible.id} phase by producing ${nextEligible.primary_output ?? `findings/${nextEligible.id}/`}`]
189
- : ["All phases complete. Review findings, open questions, and downstream implementation notes."];
190
- return {
191
- state: { ...updatedWorkspaceState, status: nextStatus },
192
- threadLogEntry: buildThreadLogEntry(validation.phaseId, validation, completionTimestamp),
193
- };
194
- });
195
- let closeoutNotice;
196
- try {
197
- const created = await ensureCloseoutStub(updatedState.workspaceDir, validation.phaseId, completionTimestamp);
198
- if (created) {
199
- closeoutNotice = `Closeout stub: .codecarto/closeouts/${closeoutFileName(dateOnly(completionTimestamp), validation.phaseId)} (fill it in)`;
200
- }
201
- }
202
- catch (error) {
203
- const message = error instanceof Error ? error.message : String(error);
204
- closeoutNotice = `Closeout stub not created: ${message}`;
205
- }
155
+ const result = await completeValidatedPhase(ctx.cwd, validation, "/codecarto-complete");
206
156
  void writeDashboard(ctx.cwd, PACKAGE_VERSION);
207
- return { updatedState, closeoutNotice };
157
+ return result;
208
158
  }
209
159
  export function decideAfterPhase(phaseStatus, phaseError, validation, strict) {
210
160
  if (phaseStatus === "aborted")
@@ -255,6 +205,18 @@ export async function runAuto(ctx, pi, initialState, options) {
255
205
  reason: "Pipeline complete.",
256
206
  });
257
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
+ }
258
220
  if (isPhaseRunning(phase.id)) {
259
221
  return finish({
260
222
  outcome: "stopped",
@@ -266,6 +228,7 @@ export async function runAuto(ctx, pi, initialState, options) {
266
228
  llmSteerEnabled,
267
229
  signal: options.signal,
268
230
  auto: true,
231
+ preflight,
269
232
  });
270
233
  // Accumulate tokens whether the phase succeeded, was aborted, or errored.
271
234
  totalTokens.input += phaseResult.activity.lifetimeUsage.input;
@@ -394,6 +357,7 @@ async function recordUsage(workspaceDir, phaseId, status, activity, sessionFile)
394
357
  output: activity.lifetimeUsage.output,
395
358
  cache_write: activity.lifetimeUsage.cacheWrite,
396
359
  },
360
+ compactions: activity.compactions,
397
361
  ...(sessionFile ? { session_file: sessionFile } : {}),
398
362
  });
399
363
  }
@@ -155,7 +155,6 @@ async function runNarratorOnce(ctx, prompt) {
155
155
  agentDir,
156
156
  sessionManager: SessionManager.inMemory(cwd),
157
157
  settingsManager: SettingsManager.create(cwd, agentDir),
158
- modelRegistry: ctx.modelRegistry,
159
158
  model: ctx.model,
160
159
  tools: [],
161
160
  resourceLoader: loader,
@@ -1,2 +1,2 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  export default function codeCartographerExtension(pi: ExtensionAPI): void;
@@ -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";
@@ -6,10 +6,32 @@ import { parseDashboardFlags } from "./dashboard-flags.js";
6
6
  import { narrateDashboard } from "./dashboard-narrator.js";
7
7
  import { writeDashboard } from "./dashboard-writer.js";
8
8
  import { parseNextFlags } from "./next-flags.js";
9
- 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";
9
+ import { phaseCompactionExtension } from "./phase-compaction.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";
10
11
  const STATUS_WIDGET_ID = "codecarto-widget";
11
12
  const STATUS_LINE_ID = "codecarto-status";
12
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
+ }
13
35
  function formatUsageTokens(count) {
14
36
  if (count >= 1_000_000)
15
37
  return `${(count / 1_000_000).toFixed(2)}M`;
@@ -31,16 +53,19 @@ function buildStatusLines(state, extraLines = []) {
31
53
  const currentPhase = nextPhase?.id ?? state.status.current_phase ?? "complete";
32
54
  const pipelineLabel = getPipelineLabel(state.status.pipeline);
33
55
  const completedCount = state.pipeline.phase_order.filter((phaseId) => state.status.phases[phaseId]?.status === "complete").length;
34
- 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);
35
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;
36
59
  const nextAction = state.status.next_actions[0] ?? (nextPhase ? `Next: ${nextPhase.id}` : "All phases complete.");
37
60
  const lines = [
38
61
  "CodeCartographer",
39
62
  `Phase: ${currentPhase}`,
63
+ `Pipeline state: ${currentPhase === "complete" ? "complete" : "in progress"}`,
40
64
  `Pipeline: ${pipelineLabel}`,
41
65
  `Progress: ${completedCount}/${state.pipeline.phase_order.length} complete`,
42
- `Open questions: ${currentOpenQuestions}`,
43
- `Carry-forward: ${totalCarryForward}`,
66
+ `Open questions (terminal unresolved): ${terminalOpenQuestions}`,
67
+ `Carry-forward (pipeline phases): ${totalCarryForward}`,
68
+ `Post-pipeline work: ${postPipelinePending} pending`,
44
69
  `Next: ${nextAction}`,
45
70
  ];
46
71
  if (extraLines.length > 0) {
@@ -62,6 +87,7 @@ function setUiState(ctx, state, extraLines = []) {
62
87
  ctx.ui.setWidget(STATUS_WIDGET_ID, buildStatusLines(state, extraLines));
63
88
  }
64
89
  export default function codeCartographerExtension(pi) {
90
+ phaseCompactionExtension(pi);
65
91
  let lastFeedbackLines = [];
66
92
  let codecartoModeActive = false;
67
93
  const readWorkspaceState = async (ctx, notifyOnError = true) => {
@@ -133,16 +159,42 @@ export default function codeCartographerExtension(pi) {
133
159
  const inputPath = typeof event.input.path === "string" ? event.input.path : "";
134
160
  const strippedPath = inputPath.startsWith("@") ? inputPath.slice(1) : inputPath;
135
161
  const targetPath = await canonicalPath(resolve(ctx.cwd, strippedPath));
136
- const allowedRoot = await canonicalPath(workspaceDir);
137
- 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))) {
138
168
  if (ctx.hasUI) {
139
- ctx.ui.notify(`Blocked ${event.toolName} outside .codecarto/: ${inputPath}`, "warning");
169
+ ctx.ui.notify(`Blocked ${event.toolName} outside .codecarto/ or configured library: ${inputPath}`, "warning");
140
170
  }
141
- 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.` };
142
172
  }
143
173
  }
144
174
  return undefined;
145
175
  });
176
+ pi.registerCommand("codecarto-open", {
177
+ description: "Activate an existing .codecarto workspace without resetting durable state",
178
+ handler: async (_args, ctx) => {
179
+ const workspaceDir = join(ctx.cwd, ".codecarto");
180
+ if (!(await pathExists(join(workspaceDir, "workflow", "status.yaml")))) {
181
+ ctx.ui.notify("No existing CodeCartographer workspace found. Run /codecarto-init first.", "warning");
182
+ return;
183
+ }
184
+ try {
185
+ const state = await getWorkspaceState(ctx.cwd);
186
+ codecartoModeActive = true;
187
+ lastFeedbackLines = [`Opened existing workspace: ${getPipelineLabel(state.status.pipeline)}`];
188
+ pi.setActiveTools(SAFE_TOOL_NAMES);
189
+ await refreshWorkspaceUi(ctx, lastFeedbackLines);
190
+ ctx.ui.notify("Opened existing CodeCartographer workspace without resetting state.", "info");
191
+ }
192
+ catch (error) {
193
+ const message = error instanceof Error ? error.message : String(error);
194
+ ctx.ui.notify(`Unable to open CodeCartographer workspace: ${message}`, "error");
195
+ }
196
+ },
197
+ });
146
198
  pi.registerCommand("codecarto-init", {
147
199
  description: "Initialize .codecarto/ in the current repository",
148
200
  getArgumentCompletions: (prefix) => {
@@ -268,6 +320,17 @@ export default function codeCartographerExtension(pi) {
268
320
  ctx.ui.notify("All CodeCartographer phases are complete.", "info");
269
321
  return;
270
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
+ }
271
334
  // Reject re-entry: don't spawn a duplicate runner for a phase that's
272
335
  // already in flight from a previous /codecarto-next invocation.
273
336
  if (isPhaseRunning(phase.id)) {
@@ -281,7 +344,7 @@ export default function codeCartographerExtension(pi) {
281
344
  // Fire-and-forget: keep the TUI responsive while the sub-agent works.
282
345
  // runSinglePhase handles all side effects (steering message, notify,
283
346
  // phase summary, recordUsage, dashboard regen, clearPhase linger).
284
- void runSinglePhase(ctx, pi, state, phase, { llmSteerEnabled, signal: ctx.signal })
347
+ void runSinglePhase(ctx, pi, state, phase, { llmSteerEnabled, signal: ctx.signal, preflight })
285
348
  .finally(() => {
286
349
  // Refresh the status widget after the phase resolves so the
287
350
  // "Open questions / Carry-forward / Next" lines reflect any
@@ -306,7 +369,15 @@ export default function codeCartographerExtension(pi) {
306
369
  ctx.ui.notify(`Unknown phase: ${phaseId}`, "error");
307
370
  return;
308
371
  }
309
- 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
+ }
310
381
  if (ctx.isIdle()) {
311
382
  pi.sendUserMessage(prompt);
312
383
  }
@@ -407,6 +478,73 @@ export default function codeCartographerExtension(pi) {
407
478
  ctx.ui.notify(`Queued CodeCartographer skill: ${skillName}`, "info");
408
479
  },
409
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
+ });
410
548
  pi.registerCommand("codecarto-usage", {
411
549
  description: "Show cumulative + per-phase token usage from local phase runs",
412
550
  handler: async (_args, ctx) => {
@@ -426,11 +564,14 @@ export default function codeCartographerExtension(pi) {
426
564
  lines.push(`Total runs: ${totals.runs}`);
427
565
  lines.push(`Total tokens: ${formatUsageTokens(totals.tokens.input)} in · ${formatUsageTokens(totals.tokens.output)} out · ${formatUsageTokens(totals.tokens.cache_write)} cache-write`);
428
566
  lines.push(`Total duration: ${formatUsageDuration(totals.duration_ms)} · ${totals.tool_uses} tool uses`);
567
+ lines.push(totals.compaction_runs > 0
568
+ ? `Compactions: ${totals.compactions.successful} successful · ${totals.compactions.failed} failed · ${totals.compactions.aborted} aborted`
569
+ : "Compactions: unavailable — historical or host usage records did not report compaction events");
429
570
  lines.push("");
430
571
  lines.push("Per-phase totals:");
431
572
  for (const [phaseId, t] of perPhase) {
432
573
  const tokensTotal = t.tokens.input + t.tokens.output;
433
- lines.push(` ${phaseId}: ${t.runs} run${t.runs === 1 ? "" : "s"} · ${formatUsageTokens(tokensTotal)} tokens · ${t.tool_uses} tool uses · ${formatUsageDuration(t.duration_ms)}`);
574
+ lines.push(` ${phaseId}: ${t.runs} run${t.runs === 1 ? "" : "s"} · ${formatUsageTokens(tokensTotal)} tokens · ${t.tool_uses} tool uses · ${t.compaction_runs > 0 ? `${t.compactions.successful + t.compactions.failed + t.compactions.aborted} compactions` : "compactions unavailable"} · ${formatUsageDuration(t.duration_ms)}`);
434
575
  }
435
576
  lastFeedbackLines = lines;
436
577
  setUiState(ctx, state, lastFeedbackLines);
@@ -0,0 +1,11 @@
1
+ import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ export declare function phaseIdFromSessionName(sessionName: string | undefined): string | null;
3
+ export declare function buildPhaseCompactionInstructions(phaseId: string, primaryOutput?: string): string;
4
+ export declare function writePhaseCheckpoint(cwd: string, phaseId: string, summary: string, tokensBefore: number): Promise<string>;
5
+ /**
6
+ * Phase-only compaction hooks shared by the parent extension and isolated
7
+ * child sessions. Keeping this as a standalone inline extension ensures that
8
+ * children spawned from an explicitly loaded (`pi -e ...`) CodeCartographer
9
+ * extension receive the same checkpoint behavior as globally installed runs.
10
+ */
11
+ export declare function phaseCompactionExtension(pi: ExtensionAPI): void;
@@ -0,0 +1,115 @@
1
+ import { mkdir, rename, writeFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { compact } from "@earendil-works/pi-coding-agent";
4
+ import { canonicalPath, getWorkspaceState, isWithinPath } from "../../core/index.js";
5
+ const PHASE_SESSION_PREFIX = "CodeCartographer phase: ";
6
+ export function phaseIdFromSessionName(sessionName) {
7
+ if (!sessionName?.startsWith(PHASE_SESSION_PREFIX))
8
+ return null;
9
+ const phaseId = sessionName.slice(PHASE_SESSION_PREFIX.length).trim();
10
+ return /^[a-z0-9][a-z0-9-]*$/.test(phaseId) ? phaseId : null;
11
+ }
12
+ export function buildPhaseCompactionInstructions(phaseId, primaryOutput) {
13
+ const output = primaryOutput ? `.codecarto/${primaryOutput}` : "the phase's declared primary output";
14
+ return [
15
+ `This is a CodeCartographer phase session for ${phaseId}.`,
16
+ `Produce a phase-aware continuation summary for ${output}.`,
17
+ "Preserve these details explicitly:",
18
+ "- phase goal and constraints",
19
+ "- evidence-backed conclusions and their evidence levels",
20
+ "- files inspected and subsystems covered or skipped",
21
+ "- primary-output sections already written and sections still missing",
22
+ "- edits already made under .codecarto/",
23
+ "- open questions and carry-forward candidates",
24
+ "- validation criteria already satisfied and validation criteria still at risk",
25
+ "- exact next steps needed to finish and validate the phase.",
26
+ "Do not turn an inference into an observed fact. Retain file paths, finding IDs, and unresolved gaps verbatim where possible.",
27
+ ].join("\n");
28
+ }
29
+ export async function writePhaseCheckpoint(cwd, phaseId, summary, tokensBefore) {
30
+ const dir = join(cwd, ".codecarto", "scratch", "checkpoints");
31
+ const target = join(dir, `${phaseId}.md`);
32
+ const temp = `${target}.${process.pid}.${Date.now()}.tmp`;
33
+ await mkdir(dir, { recursive: true });
34
+ const content = [
35
+ "---",
36
+ `phase: ${phaseId}`,
37
+ `updated_at: ${new Date().toISOString()}`,
38
+ `tokens_before: ${tokensBefore}`,
39
+ "source: pi-compaction",
40
+ "---",
41
+ "",
42
+ "# Phase checkpoint",
43
+ "",
44
+ summary.trim(),
45
+ "",
46
+ ].join("\n");
47
+ await writeFile(temp, content, "utf8");
48
+ await rename(temp, target);
49
+ return target;
50
+ }
51
+ /**
52
+ * Phase-only compaction hooks shared by the parent extension and isolated
53
+ * child sessions. Keeping this as a standalone inline extension ensures that
54
+ * children spawned from an explicitly loaded (`pi -e ...`) CodeCartographer
55
+ * extension receive the same checkpoint behavior as globally installed runs.
56
+ */
57
+ export function phaseCompactionExtension(pi) {
58
+ pi.on("tool_call", async (event, ctx) => {
59
+ if (!phaseIdFromSessionName(ctx.sessionManager.getSessionName()))
60
+ return undefined;
61
+ if (event.toolName === "bash") {
62
+ return { block: true, reason: "CodeCartographer phase sessions disable bash to keep source analysis read-only." };
63
+ }
64
+ if (event.toolName === "edit" || event.toolName === "write") {
65
+ const inputPath = typeof event.input.path === "string" ? event.input.path : "";
66
+ const strippedPath = inputPath.startsWith("@") ? inputPath.slice(1) : inputPath;
67
+ const targetPath = await canonicalPath(resolve(ctx.cwd, strippedPath));
68
+ const allowedRoot = await canonicalPath(join(ctx.cwd, ".codecarto"));
69
+ if (!isWithinPath(targetPath, allowedRoot)) {
70
+ return {
71
+ block: true,
72
+ reason: `CodeCartographer phase sessions only allow ${event.toolName} within .codecarto/`,
73
+ };
74
+ }
75
+ }
76
+ return undefined;
77
+ });
78
+ pi.on("session_before_compact", async (event, ctx) => {
79
+ const phaseId = phaseIdFromSessionName(ctx.sessionManager.getSessionName());
80
+ if (!phaseId || !ctx.model)
81
+ return undefined;
82
+ try {
83
+ const state = await getWorkspaceState(ctx.cwd);
84
+ const phase = state.pipeline.phases.find((candidate) => candidate.id === phaseId);
85
+ const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
86
+ if (!auth.ok || !auth.apiKey)
87
+ return undefined;
88
+ const instructions = buildPhaseCompactionInstructions(phaseId, phase?.primary_output);
89
+ const result = await compact(event.preparation, ctx.model, auth.apiKey, auth.headers, instructions, event.signal);
90
+ return { compaction: result };
91
+ }
92
+ catch (error) {
93
+ if (ctx.hasUI) {
94
+ const message = error instanceof Error ? error.message : String(error);
95
+ ctx.ui.notify(`Phase-aware compaction unavailable (${message}); using host default.`, "warning");
96
+ }
97
+ return undefined;
98
+ }
99
+ });
100
+ pi.on("session_compact", async (event, ctx) => {
101
+ const phaseId = phaseIdFromSessionName(ctx.sessionManager.getSessionName());
102
+ if (!phaseId)
103
+ return;
104
+ try {
105
+ await writePhaseCheckpoint(ctx.cwd, phaseId, event.compactionEntry.summary, event.compactionEntry.tokensBefore);
106
+ }
107
+ catch (error) {
108
+ const message = error instanceof Error ? error.message : String(error);
109
+ if (ctx.hasUI)
110
+ ctx.ui.notify(`Phase checkpoint could not be written: ${message}`, "warning");
111
+ else
112
+ console.warn(`[codecarto] Phase checkpoint could not be written: ${message}`);
113
+ }
114
+ });
115
+ }