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.
- package/.codecarto/GUIDE.md +10 -3
- package/.codecarto/findings/porting/SKILL.md +7 -0
- package/.codecarto/findings/reimplementation-spec/SKILL.md +10 -0
- package/.codecarto/templates/architecture-map.md +9 -0
- package/.codecarto/templates/behavioral-contracts.md +9 -0
- package/.codecarto/templates/defect-report.md +9 -0
- package/.codecarto/templates/mechanical-defects.md +9 -0
- package/.codecarto/templates/phase-checkpoint.md +41 -0
- package/.codecarto/templates/protocols-and-state.md +9 -0
- package/.codecarto/templates/reimplementation-spec-opinionated.md +10 -0
- package/.codecarto/templates/reimplementation-spec.md +10 -0
- package/.codecarto/templates/reverse-engineering-bundle.md +29 -3
- package/.codecarto/templates/semantic-defects.md +9 -0
- package/.codecarto/workflow/pipeline-architecture-only.yaml +1 -0
- package/.codecarto/workflow/pipeline-defect-scan.yaml +2 -0
- package/.codecarto/workflow/pipeline-full-with-audit.yaml +8 -3
- package/.codecarto/workflow/pipeline-full-with-deep-audit.yaml +9 -5
- package/.codecarto/workflow/pipeline-lite.yaml +3 -0
- package/.codecarto/workflow/pipeline.yaml +7 -3
- package/README.md +38 -2
- package/dist/core/dashboard.js +178 -15
- package/dist/core/prompts.js +6 -0
- package/dist/core/usage.d.ts +11 -0
- package/dist/core/usage.js +64 -46
- package/dist/extensions/codecarto/agent-rewriter.js +0 -1
- package/dist/extensions/codecarto/agent-runner.d.ts +19 -0
- package/dist/extensions/codecarto/agent-runner.js +68 -6
- package/dist/extensions/codecarto/agent-state.d.ts +3 -0
- package/dist/extensions/codecarto/agent-state.js +2 -0
- package/dist/extensions/codecarto/agent-summary.d.ts +5 -0
- package/dist/extensions/codecarto/agent-summary.js +9 -0
- package/dist/extensions/codecarto/agent-widget.js +6 -0
- package/dist/extensions/codecarto/auto-runner.js +13 -1
- package/dist/extensions/codecarto/dashboard-narrator.js +0 -1
- package/dist/extensions/codecarto/index.d.ts +1 -1
- package/dist/extensions/codecarto/index.js +28 -1
- package/dist/extensions/codecarto/phase-compaction.d.ts +11 -0
- package/dist/extensions/codecarto/phase-compaction.js +115 -0
- 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:
|
|
34
|
-
//
|
|
35
|
-
//
|
|
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:
|
|
40
|
-
noSkills:
|
|
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;
|
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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": "
|
|
46
|
+
"@earendil-works/pi-coding-agent": "^0.80.10",
|
|
47
47
|
"@sinclair/typebox": "*"
|
|
48
48
|
},
|
|
49
49
|
"pi": {
|