codecartographer-pi 0.9.1 → 0.11.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 +10 -3
  2. package/.codecarto/findings/porting/SKILL.md +7 -0
  3. package/.codecarto/findings/reimplementation-spec/SKILL.md +10 -0
  4. package/.codecarto/templates/architecture-map.md +9 -0
  5. package/.codecarto/templates/behavioral-contracts.md +9 -0
  6. package/.codecarto/templates/defect-report.md +9 -0
  7. package/.codecarto/templates/mechanical-defects.md +9 -0
  8. package/.codecarto/templates/phase-checkpoint.md +41 -0
  9. package/.codecarto/templates/protocols-and-state.md +9 -0
  10. package/.codecarto/templates/reimplementation-spec-opinionated.md +10 -0
  11. package/.codecarto/templates/reimplementation-spec.md +10 -0
  12. package/.codecarto/templates/reverse-engineering-bundle.md +29 -3
  13. package/.codecarto/templates/semantic-defects.md +9 -0
  14. package/.codecarto/workflow/pipeline-architecture-only.yaml +1 -0
  15. package/.codecarto/workflow/pipeline-defect-scan.yaml +2 -0
  16. package/.codecarto/workflow/pipeline-full-with-audit.yaml +8 -3
  17. package/.codecarto/workflow/pipeline-full-with-deep-audit.yaml +9 -5
  18. package/.codecarto/workflow/pipeline-lite.yaml +3 -0
  19. package/.codecarto/workflow/pipeline.yaml +7 -3
  20. package/README.md +38 -2
  21. package/dist/core/dashboard.js +178 -15
  22. package/dist/core/prompts.js +6 -0
  23. package/dist/core/usage.d.ts +11 -0
  24. package/dist/core/usage.js +64 -46
  25. package/dist/extensions/codecarto/agent-rewriter.js +0 -1
  26. package/dist/extensions/codecarto/agent-runner.d.ts +19 -0
  27. package/dist/extensions/codecarto/agent-runner.js +68 -6
  28. package/dist/extensions/codecarto/agent-state.d.ts +3 -0
  29. package/dist/extensions/codecarto/agent-state.js +2 -0
  30. package/dist/extensions/codecarto/agent-summary.d.ts +5 -0
  31. package/dist/extensions/codecarto/agent-summary.js +9 -0
  32. package/dist/extensions/codecarto/agent-widget.js +6 -0
  33. package/dist/extensions/codecarto/auto-runner.js +13 -1
  34. package/dist/extensions/codecarto/dashboard-narrator.js +0 -1
  35. package/dist/extensions/codecarto/index.d.ts +1 -1
  36. package/dist/extensions/codecarto/index.js +28 -1
  37. package/dist/extensions/codecarto/phase-compaction.d.ts +11 -0
  38. package/dist/extensions/codecarto/phase-compaction.js +115 -0
  39. package/package.json +2 -2
@@ -1,4 +1,15 @@
1
1
  import { type AgentSession, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ export declare function needsPhaseContinuation(messages: ReadonlyArray<{
3
+ role: string;
4
+ stopReason?: string;
5
+ }>): boolean;
6
+ export declare function shouldContinuePhase(messages: ReadonlyArray<{
7
+ role: string;
8
+ stopReason?: string;
9
+ }>, primaryOutputPresent: boolean): boolean;
10
+ export declare function primaryOutputExists(cwd: string, primaryOutput: string): Promise<boolean>;
11
+ export declare function buildPhaseContinuationPrompt(compacted: boolean): string;
12
+ export declare function waitForCompaction(compactionCompleted: Promise<boolean>, timeoutMs?: number): Promise<boolean>;
2
13
  export interface PhaseRunCallbacks {
3
14
  onSessionCreated?: (session: AgentSession) => void;
4
15
  onToolStart?: (toolCallId: string, toolName: string) => void;
@@ -10,12 +21,20 @@ export interface PhaseRunCallbacks {
10
21
  output: number;
11
22
  cacheWrite: number;
12
23
  }) => void;
24
+ onCompactionEnd?: (event: {
25
+ reason: "manual" | "threshold" | "overflow";
26
+ successful: boolean;
27
+ aborted: boolean;
28
+ }) => void;
13
29
  }
14
30
  export interface PhaseRunOptions {
15
31
  /** Display name written via appendSessionInfo so the session shows up in
16
32
  * /resume's picker as e.g. "CodeCartographer phase: blueprint". Pi reads
17
33
  * it via SessionManager.getSessionName(). */
18
34
  sessionName?: string;
35
+ /** Primary output relative to `.codecarto/`; used to detect provider runs
36
+ * that stop normally before writing their required artifact. */
37
+ primaryOutput?: string;
19
38
  }
20
39
  export interface PhaseRunResult {
21
40
  session: AgentSession;
@@ -9,11 +9,51 @@
9
9
  // system prompt, no parent-context inheritance, no turn-limit grace logic.
10
10
  // Codecarto phases are bounded by their phase prompt and validation gate;
11
11
  // they don't need the full subagent-framework machinery.
12
+ import { access } from "node:fs/promises";
13
+ import { join, resolve } from "node:path";
12
14
  import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
15
+ import { canonicalPath, isWithinPath } from "../../core/index.js";
16
+ import { phaseCompactionExtension } from "./phase-compaction.js";
13
17
  // Tools available to the phase sub-agent. Matches the codecarto interception
14
18
  // allowlist (SAFE_TOOL_NAMES in extensions/codecarto/index.ts), minus bash.
15
19
  // Phases analyze source code and write findings; they don't need a shell.
16
20
  const PHASE_TOOL_NAMES = ["read", "edit", "write", "grep", "find", "ls"];
21
+ const COMPACTION_SETTLE_TIMEOUT_MS = 30_000;
22
+ export function needsPhaseContinuation(messages) {
23
+ const last = messages.at(-1);
24
+ if (!last)
25
+ return false;
26
+ return last.role === "toolResult" || (last.role === "assistant" && last.stopReason === "toolUse");
27
+ }
28
+ export function shouldContinuePhase(messages, primaryOutputPresent) {
29
+ return !primaryOutputPresent || needsPhaseContinuation(messages);
30
+ }
31
+ export async function primaryOutputExists(cwd, primaryOutput) {
32
+ const workspaceRoot = await canonicalPath(join(cwd, ".codecarto"));
33
+ const candidate = await canonicalPath(resolve(workspaceRoot, primaryOutput));
34
+ if (!isWithinPath(candidate, workspaceRoot))
35
+ return false;
36
+ return access(candidate).then(() => true, () => false);
37
+ }
38
+ export function buildPhaseContinuationPrompt(compacted) {
39
+ const recovery = compacted
40
+ ? "Continue the current CodeCartographer phase from the compacted context and durable checkpoint."
41
+ : "The previous phase run stopped before finalizing its required output. Continue from the current session context.";
42
+ return `${recovery} Finish the declared primary output, validation block, status updates, and closeout before ending.`;
43
+ }
44
+ export async function waitForCompaction(compactionCompleted, timeoutMs = COMPACTION_SETTLE_TIMEOUT_MS) {
45
+ let timer;
46
+ try {
47
+ return await Promise.race([
48
+ compactionCompleted,
49
+ new Promise((resolve) => { timer = setTimeout(() => resolve(false), timeoutMs); }),
50
+ ]);
51
+ }
52
+ finally {
53
+ if (timer)
54
+ clearTimeout(timer);
55
+ }
56
+ }
17
57
  /**
18
58
  * Run one CodeCartographer phase as an isolated AgentSession. Awaiting this
19
59
  * function blocks until the phase completes (or aborts via signal). The
@@ -30,17 +70,19 @@ const PHASE_TOOL_NAMES = ["read", "edit", "write", "grep", "find", "ls"];
30
70
  export async function runPhase(ctx, prompt, callbacks = {}, options = {}, signal) {
31
71
  const cwd = ctx.cwd;
32
72
  const agentDir = getAgentDir();
33
- // Resource loader: load Pi extensions and skills (so codecarto's own tool
34
- // interception applies to the child) but skip prompt templates, themes,
35
- // and project context files — they'd just bloat the system prompt.
73
+ // Resource loader: isolate the child from global extensions/skills and load
74
+ // only CodeCartographer's inline phase guards and compaction hooks. This
75
+ // avoids duplicate registration when CodeCartographer is globally installed
76
+ // while preserving the same safety when it was loaded explicitly with -e.
36
77
  const loader = new DefaultResourceLoader({
37
78
  cwd,
38
79
  agentDir,
39
- noExtensions: false,
40
- noSkills: false,
80
+ noExtensions: true,
81
+ noSkills: true,
41
82
  noPromptTemplates: true,
42
83
  noThemes: true,
43
84
  noContextFiles: true,
85
+ extensionFactories: [phaseCompactionExtension],
44
86
  });
45
87
  await loader.reload();
46
88
  // File-backed session in the same directory the orchestrator's TUI uses.
@@ -64,7 +106,6 @@ export async function runPhase(ctx, prompt, callbacks = {}, options = {}, signal
64
106
  agentDir,
65
107
  sessionManager,
66
108
  settingsManager: SettingsManager.create(cwd, agentDir),
67
- modelRegistry: ctx.modelRegistry,
68
109
  model: ctx.model,
69
110
  tools: PHASE_TOOL_NAMES,
70
111
  resourceLoader: loader,
@@ -75,6 +116,8 @@ export async function runPhase(ctx, prompt, callbacks = {}, options = {}, signal
75
116
  let turnCount = 0;
76
117
  let currentMessageText = "";
77
118
  let aborted = false;
119
+ let resolveCompaction;
120
+ const compactionCompleted = new Promise((resolve) => { resolveCompaction = resolve; });
78
121
  const unsubscribe = session.subscribe((event) => {
79
122
  switch (event.type) {
80
123
  case "tool_execution_start": {
@@ -117,6 +160,16 @@ export async function runPhase(ctx, prompt, callbacks = {}, options = {}, signal
117
160
  }
118
161
  break;
119
162
  }
163
+ case "compaction_end": {
164
+ const compactEvent = event;
165
+ callbacks.onCompactionEnd?.({
166
+ reason: compactEvent.reason,
167
+ successful: compactEvent.result !== undefined && compactEvent.result !== null && !compactEvent.aborted && !compactEvent.errorMessage,
168
+ aborted: compactEvent.aborted,
169
+ });
170
+ resolveCompaction?.(true);
171
+ break;
172
+ }
120
173
  }
121
174
  });
122
175
  let abortCleanup = () => { };
@@ -130,6 +183,15 @@ export async function runPhase(ctx, prompt, callbacks = {}, options = {}, signal
130
183
  }
131
184
  try {
132
185
  await session.prompt(prompt);
186
+ let primaryOutputPresent = true;
187
+ if (options.primaryOutput) {
188
+ primaryOutputPresent = await primaryOutputExists(cwd, options.primaryOutput);
189
+ }
190
+ if (!aborted && shouldContinuePhase(session.messages, primaryOutputPresent)) {
191
+ const compacted = await waitForCompaction(compactionCompleted);
192
+ if (!aborted)
193
+ await session.prompt(buildPhaseContinuationPrompt(compacted));
194
+ }
133
195
  }
134
196
  finally {
135
197
  unsubscribe();
@@ -1,4 +1,5 @@
1
1
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
2
+ import { type CompactionTelemetry } from "../../core/usage.ts";
2
3
  export type PhaseStatus = "running" | "completed" | "error" | "aborted";
3
4
  export interface PhaseActivity {
4
5
  phaseId: string;
@@ -17,6 +18,8 @@ export interface PhaseActivity {
17
18
  output: number;
18
19
  cacheWrite: number;
19
20
  };
21
+ /** Compaction outcomes observed during this phase session. */
22
+ compactions: CompactionTelemetry;
20
23
  session?: AgentSession;
21
24
  error?: string;
22
25
  }
@@ -2,6 +2,7 @@
2
2
  // it from session-event callbacks; the agents widget (M2) reads it on each
3
3
  // render. Module-scoped Map so different command handlers can hand work to
4
4
  // the runner and the widget sees the same state without explicit plumbing.
5
+ import { emptyCompactionTelemetry } from "../../core/usage.js";
5
6
  const phaseActivity = new Map();
6
7
  export function getPhaseActivity(phaseId) {
7
8
  return phaseActivity.get(phaseId);
@@ -22,6 +23,7 @@ export function startPhase(phaseId) {
22
23
  activeTools: new Map(),
23
24
  responseText: "",
24
25
  lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
26
+ compactions: emptyCompactionTelemetry(),
25
27
  };
26
28
  phaseActivity.set(phaseId, activity);
27
29
  return activity;
@@ -9,6 +9,11 @@ export interface PhaseSummaryInput {
9
9
  output: number;
10
10
  cacheWrite: number;
11
11
  };
12
+ compactions?: {
13
+ successful: number;
14
+ failed: number;
15
+ aborted: number;
16
+ };
12
17
  durationMs: number;
13
18
  responseText: string;
14
19
  sessionFile?: string;
@@ -39,6 +39,15 @@ function formatStats(input) {
39
39
  const totalTokens = input.tokens.input + input.tokens.output;
40
40
  if (totalTokens > 0)
41
41
  parts.push(formatTokens(totalTokens));
42
+ const compact = input.compactions;
43
+ if (compact && compact.successful + compact.failed + compact.aborted > 0) {
44
+ const details = [`${compact.successful} compaction${compact.successful === 1 ? "" : "s"}`];
45
+ if (compact.failed > 0)
46
+ details.push(`${compact.failed} failed`);
47
+ if (compact.aborted > 0)
48
+ details.push(`${compact.aborted} aborted`);
49
+ parts.push(details.join(", "));
50
+ }
42
51
  if (input.durationMs > 0)
43
52
  parts.push(formatDuration(input.durationMs));
44
53
  return parts.length > 0 ? `_${parts.join(" · ")}_` : "_(no activity recorded)_";
@@ -186,6 +186,9 @@ function formatRunningStats(a) {
186
186
  const tokens = a.lifetimeUsage.input + a.lifetimeUsage.output;
187
187
  if (tokens > 0)
188
188
  parts.push(formatTokens(tokens));
189
+ const compactions = a.compactions.successful + a.compactions.failed + a.compactions.aborted;
190
+ if (compactions > 0)
191
+ parts.push(`${compactions} compact`);
189
192
  parts.push(formatDuration(Date.now() - a.startedAt));
190
193
  return parts.join(" · ");
191
194
  }
@@ -198,6 +201,9 @@ function formatFinishedStats(a) {
198
201
  const tokens = a.lifetimeUsage.input + a.lifetimeUsage.output;
199
202
  if (tokens > 0)
200
203
  parts.push(formatTokens(tokens));
204
+ const compactions = a.compactions.successful + a.compactions.failed + a.compactions.aborted;
205
+ if (compactions > 0)
206
+ parts.push(`${compactions} compact`);
201
207
  const dur = a.completedAt ? a.completedAt - a.startedAt : 0;
202
208
  if (dur > 0)
203
209
  parts.push(formatDuration(dur));
@@ -69,7 +69,16 @@ export async function runSinglePhase(ctx, pi, state, phase, options) {
69
69
  activity.lifetimeUsage.output += usage.output;
70
70
  activity.lifetimeUsage.cacheWrite += usage.cacheWrite;
71
71
  },
72
- }, { sessionName: `CodeCartographer phase: ${phase.id}` }, options.signal);
72
+ onCompactionEnd: (event) => {
73
+ activity.compactions.reasons[event.reason]++;
74
+ if (event.aborted)
75
+ activity.compactions.aborted++;
76
+ else if (event.successful)
77
+ activity.compactions.successful++;
78
+ else
79
+ activity.compactions.failed++;
80
+ },
81
+ }, { sessionName: `CodeCartographer phase: ${phase.id}`, primaryOutput: phase.primary_output }, options.signal);
73
82
  const status = result.aborted ? "aborted" : "completed";
74
83
  finishPhase(phase.id, { status });
75
84
  if (ctx.hasUI) {
@@ -85,6 +94,7 @@ export async function runSinglePhase(ctx, pi, state, phase, options) {
85
94
  turnCount: activity.turnCount,
86
95
  toolUses: activity.toolUses,
87
96
  tokens: activity.lifetimeUsage,
97
+ compactions: activity.compactions,
88
98
  durationMs: (activity.completedAt ?? Date.now()) - activity.startedAt,
89
99
  responseText: result.responseText,
90
100
  sessionFile: result.sessionFile,
@@ -113,6 +123,7 @@ export async function runSinglePhase(ctx, pi, state, phase, options) {
113
123
  turnCount: activity.turnCount,
114
124
  toolUses: activity.toolUses,
115
125
  tokens: activity.lifetimeUsage,
126
+ compactions: activity.compactions,
116
127
  durationMs: (activity.completedAt ?? Date.now()) - activity.startedAt,
117
128
  responseText: "",
118
129
  error: message,
@@ -394,6 +405,7 @@ async function recordUsage(workspaceDir, phaseId, status, activity, sessionFile)
394
405
  output: activity.lifetimeUsage.output,
395
406
  cache_write: activity.lifetimeUsage.cacheWrite,
396
407
  },
408
+ compactions: activity.compactions,
397
409
  ...(sessionFile ? { session_file: sessionFile } : {}),
398
410
  });
399
411
  }
@@ -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;
@@ -6,6 +6,7 @@ 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 { phaseCompactionExtension } from "./phase-compaction.js";
9
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
11
  const STATUS_WIDGET_ID = "codecarto-widget";
11
12
  const STATUS_LINE_ID = "codecarto-status";
@@ -62,6 +63,7 @@ function setUiState(ctx, state, extraLines = []) {
62
63
  ctx.ui.setWidget(STATUS_WIDGET_ID, buildStatusLines(state, extraLines));
63
64
  }
64
65
  export default function codeCartographerExtension(pi) {
66
+ phaseCompactionExtension(pi);
65
67
  let lastFeedbackLines = [];
66
68
  let codecartoModeActive = false;
67
69
  const readWorkspaceState = async (ctx, notifyOnError = true) => {
@@ -143,6 +145,28 @@ export default function codeCartographerExtension(pi) {
143
145
  }
144
146
  return undefined;
145
147
  });
148
+ pi.registerCommand("codecarto-open", {
149
+ description: "Activate an existing .codecarto workspace without resetting durable state",
150
+ handler: async (_args, ctx) => {
151
+ const workspaceDir = join(ctx.cwd, ".codecarto");
152
+ if (!(await pathExists(join(workspaceDir, "workflow", "status.yaml")))) {
153
+ ctx.ui.notify("No existing CodeCartographer workspace found. Run /codecarto-init first.", "warning");
154
+ return;
155
+ }
156
+ try {
157
+ const state = await getWorkspaceState(ctx.cwd);
158
+ codecartoModeActive = true;
159
+ lastFeedbackLines = [`Opened existing workspace: ${getPipelineLabel(state.status.pipeline)}`];
160
+ pi.setActiveTools(SAFE_TOOL_NAMES);
161
+ await refreshWorkspaceUi(ctx, lastFeedbackLines);
162
+ ctx.ui.notify("Opened existing CodeCartographer workspace without resetting state.", "info");
163
+ }
164
+ catch (error) {
165
+ const message = error instanceof Error ? error.message : String(error);
166
+ ctx.ui.notify(`Unable to open CodeCartographer workspace: ${message}`, "error");
167
+ }
168
+ },
169
+ });
146
170
  pi.registerCommand("codecarto-init", {
147
171
  description: "Initialize .codecarto/ in the current repository",
148
172
  getArgumentCompletions: (prefix) => {
@@ -426,11 +450,14 @@ export default function codeCartographerExtension(pi) {
426
450
  lines.push(`Total runs: ${totals.runs}`);
427
451
  lines.push(`Total tokens: ${formatUsageTokens(totals.tokens.input)} in · ${formatUsageTokens(totals.tokens.output)} out · ${formatUsageTokens(totals.tokens.cache_write)} cache-write`);
428
452
  lines.push(`Total duration: ${formatUsageDuration(totals.duration_ms)} · ${totals.tool_uses} tool uses`);
453
+ lines.push(totals.compaction_runs > 0
454
+ ? `Compactions: ${totals.compactions.successful} successful · ${totals.compactions.failed} failed · ${totals.compactions.aborted} aborted`
455
+ : "Compactions: unavailable — historical or host usage records did not report compaction events");
429
456
  lines.push("");
430
457
  lines.push("Per-phase totals:");
431
458
  for (const [phaseId, t] of perPhase) {
432
459
  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)}`);
460
+ 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
461
  }
435
462
  lastFeedbackLines = lines;
436
463
  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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecartographer-pi",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "CodeCartographer packaged for Pi as an extension-driven workflow wrapper.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -43,7 +43,7 @@
43
43
  "@modelcontextprotocol/sdk": "^1.29.0"
44
44
  },
45
45
  "peerDependencies": {
46
- "@earendil-works/pi-coding-agent": "~0.74.0",
46
+ "@earendil-works/pi-coding-agent": "^0.80.10",
47
47
  "@sinclair/typebox": "*"
48
48
  },
49
49
  "pi": {