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