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.
- package/.codecarto/GUIDE.md +40 -18
- package/.codecarto/README.md +3 -0
- package/.codecarto/findings/goal-synthesis/README.md +3 -0
- package/.codecarto/findings/goal-synthesis-finalize/SKILL.md +30 -0
- package/.codecarto/findings/goal-synthesis-propose/SKILL.md +24 -0
- package/.codecarto/findings/porting/SKILL.md +7 -0
- package/.codecarto/findings/reimplementation-spec/SKILL.md +10 -0
- package/.codecarto/findings/spec-merge/README.md +3 -0
- package/.codecarto/findings/spec-merge/SKILL.md +23 -0
- package/.codecarto/findings/vision-capture/README.md +3 -0
- package/.codecarto/findings/vision-capture/SKILL.md +26 -0
- package/.codecarto/inputs/vision.md +11 -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/merged-spec.md +58 -0
- package/.codecarto/templates/phase-checkpoint.md +41 -0
- package/.codecarto/templates/phase-handoff.yaml +22 -0
- package/.codecarto/templates/project-plan.md +70 -0
- package/.codecarto/templates/proposal.md +43 -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/templates/vision.md +63 -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-synthesis.yaml +105 -0
- package/.codecarto/workflow/pipeline.yaml +7 -3
- package/.codecarto/workflow/status.yaml +2 -0
- package/README.md +89 -7
- package/dist/core/completion.d.ts +6 -0
- package/dist/core/completion.js +127 -0
- package/dist/core/dashboard.js +37 -7
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/pipeline.js +1 -0
- package/dist/core/prompts.d.ts +9 -3
- package/dist/core/prompts.js +43 -26
- package/dist/core/status.d.ts +7 -1
- package/dist/core/status.js +187 -2
- package/dist/core/synthesis.d.ts +31 -0
- package/dist/core/synthesis.js +140 -0
- package/dist/core/types.d.ts +29 -1
- package/dist/core/usage.d.ts +11 -0
- package/dist/core/usage.js +64 -46
- package/dist/core/workspace.d.ts +3 -1
- package/dist/core/workspace.js +39 -3
- package/dist/core/yaml.js +24 -0
- 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.d.ts +3 -1
- package/dist/extensions/codecarto/auto-runner.js +33 -69
- package/dist/extensions/codecarto/dashboard-narrator.js +0 -1
- package/dist/extensions/codecarto/index.d.ts +1 -1
- package/dist/extensions/codecarto/index.js +153 -12
- package/dist/extensions/codecarto/phase-compaction.d.ts +11 -0
- package/dist/extensions/codecarto/phase-compaction.js +115 -0
- package/dist/mcp-server/server.js +24 -68
- 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,
|
|
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, {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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: ${
|
|
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
|
|
137
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|