codecartographer-pi 0.6.1 → 0.9.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 +2 -0
- package/README.md +275 -219
- package/assets/logo.svg +42 -0
- package/dist/core/dashboard.d.ts +42 -0
- package/dist/core/dashboard.js +637 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/library.d.ts +157 -0
- package/dist/core/library.js +675 -0
- package/dist/core/orchestrator-config.d.ts +38 -0
- package/dist/core/orchestrator-config.js +86 -19
- package/dist/core/pipeline.js +15 -3
- package/dist/core/prompts.d.ts +12 -1
- package/dist/core/prompts.js +15 -6
- package/dist/core/usage.js +15 -1
- package/dist/core/utils.d.ts +21 -0
- package/dist/core/utils.js +44 -1
- package/dist/core/workspace.d.ts +1 -0
- package/dist/core/workspace.js +13 -1
- package/dist/extensions/codecarto/auto-runner.d.ts +96 -0
- package/dist/extensions/codecarto/auto-runner.js +403 -0
- package/dist/extensions/codecarto/dashboard-flags.d.ts +6 -0
- package/dist/extensions/codecarto/dashboard-flags.js +17 -0
- package/dist/extensions/codecarto/dashboard-narrator.d.ts +8 -0
- package/dist/extensions/codecarto/dashboard-narrator.js +182 -0
- package/dist/extensions/codecarto/dashboard-writer.d.ts +1 -0
- package/dist/extensions/codecarto/dashboard-writer.js +148 -0
- package/dist/extensions/codecarto/index.js +98 -208
- package/dist/extensions/codecarto/next-flags.d.ts +4 -0
- package/dist/extensions/codecarto/next-flags.js +19 -6
- package/dist/mcp-server/server.d.ts +21 -0
- package/dist/mcp-server/server.js +305 -3
- package/package.json +3 -2
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// I/O wrapper that gathers all dashboard inputs and writes the rendered
|
|
2
|
+
// HTML to `.codecarto/dashboard.html`. Best-effort — failures are swallowed
|
|
3
|
+
// and never escalate to a phase error the user sees, mirroring the
|
|
4
|
+
// recordUsage discipline at extensions/codecarto/index.ts.
|
|
5
|
+
import { readdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { DASHBOARD_RELATIVE_PATH, getWorkspaceState, loadUsage, NARRATION_CACHE_RELATIVE_PATH, parseSimpleYaml, pathExists, renderDashboard, } from "../../core/index.js";
|
|
8
|
+
const CLOSEOUT_FILENAME_RE = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
|
|
9
|
+
export async function writeDashboard(cwd, packageVersion) {
|
|
10
|
+
try {
|
|
11
|
+
const state = await getWorkspaceState(cwd);
|
|
12
|
+
if (!state)
|
|
13
|
+
return;
|
|
14
|
+
const workspaceDir = state.workspaceDir;
|
|
15
|
+
const [usage, closeouts, outputsPresent, narration] = await Promise.all([
|
|
16
|
+
loadUsage(workspaceDir),
|
|
17
|
+
listCloseouts(workspaceDir),
|
|
18
|
+
buildOutputsPresent(state.workspaceDir, state.pipeline),
|
|
19
|
+
loadNarration(workspaceDir),
|
|
20
|
+
]);
|
|
21
|
+
const inputs = {
|
|
22
|
+
status: state.status,
|
|
23
|
+
pipeline: state.pipeline,
|
|
24
|
+
usage,
|
|
25
|
+
closeouts,
|
|
26
|
+
outputsPresent,
|
|
27
|
+
packageVersion,
|
|
28
|
+
generatedAt: new Date().toISOString(),
|
|
29
|
+
narration,
|
|
30
|
+
};
|
|
31
|
+
const html = renderDashboard(inputs);
|
|
32
|
+
const path = join(workspaceDir, DASHBOARD_RELATIVE_PATH);
|
|
33
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
34
|
+
await writeFile(tempPath, html, "utf8");
|
|
35
|
+
await rename(tempPath, path);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Best-effort: a failed dashboard write must not surface as a phase
|
|
39
|
+
// error. The user's pipeline state is unaffected; the next state
|
|
40
|
+
// change will trigger another render attempt.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function listCloseouts(workspaceDir) {
|
|
44
|
+
const dir = join(workspaceDir, "closeouts");
|
|
45
|
+
if (!(await pathExists(dir)))
|
|
46
|
+
return [];
|
|
47
|
+
let entries;
|
|
48
|
+
try {
|
|
49
|
+
entries = await readdir(dir);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const name of entries) {
|
|
56
|
+
const m = CLOSEOUT_FILENAME_RE.exec(name);
|
|
57
|
+
if (!m)
|
|
58
|
+
continue;
|
|
59
|
+
out.push({ date: m[1], phaseOrModule: m[2], fileName: name, summary: await readCloseoutSummary(join(dir, name)) });
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
async function readCloseoutSummary(path) {
|
|
64
|
+
try {
|
|
65
|
+
const raw = await readFile(path, "utf8");
|
|
66
|
+
const lines = raw.split(/\r?\n/);
|
|
67
|
+
const summaryStart = lines.findIndex((line) => /^##\s+Summary\s*$/i.test(line.trim()));
|
|
68
|
+
if (summaryStart === -1)
|
|
69
|
+
return undefined;
|
|
70
|
+
const body = [];
|
|
71
|
+
for (const line of lines.slice(summaryStart + 1)) {
|
|
72
|
+
if (/^##\s+/.test(line.trim()))
|
|
73
|
+
break;
|
|
74
|
+
const trimmed = line.trim();
|
|
75
|
+
if (!trimmed || trimmed === "-")
|
|
76
|
+
continue;
|
|
77
|
+
body.push(trimmed.replace(/^[-*]\s+/, ""));
|
|
78
|
+
if (body.join(" ").length > 280)
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
const summary = body.join(" ").trim();
|
|
82
|
+
return summary ? `${summary.slice(0, 280)}${summary.length > 280 ? "…" : ""}` : undefined;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function buildOutputsPresent(workspaceDir, pipeline) {
|
|
89
|
+
const out = new Map();
|
|
90
|
+
for (const phaseId of pipeline.phase_order) {
|
|
91
|
+
const phaseDef = pipeline.phases.find((p) => p.id === phaseId);
|
|
92
|
+
if (!phaseDef)
|
|
93
|
+
continue;
|
|
94
|
+
const entry = { secondary: [] };
|
|
95
|
+
if (phaseDef.primary_output) {
|
|
96
|
+
entry.primary = {
|
|
97
|
+
path: phaseDef.primary_output,
|
|
98
|
+
exists: await pathExists(join(workspaceDir, phaseDef.primary_output)),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
for (const sec of phaseDef.secondary_outputs ?? []) {
|
|
102
|
+
entry.secondary.push({
|
|
103
|
+
path: sec.path,
|
|
104
|
+
exists: await pathExists(join(workspaceDir, sec.path)),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
out.set(phaseId, entry);
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
async function loadNarration(workspaceDir) {
|
|
112
|
+
const path = join(workspaceDir, NARRATION_CACHE_RELATIVE_PATH);
|
|
113
|
+
if (!(await pathExists(path)))
|
|
114
|
+
return undefined;
|
|
115
|
+
try {
|
|
116
|
+
const raw = await readFile(path, "utf8");
|
|
117
|
+
const { frontmatter, body } = splitFrontmatter(raw);
|
|
118
|
+
if (!frontmatter)
|
|
119
|
+
return undefined;
|
|
120
|
+
const generatedAt = typeof frontmatter.generatedAt === "string" ? frontmatter.generatedAt : "";
|
|
121
|
+
const phaseCountAtGeneration = typeof frontmatter.phaseCountAtGeneration === "number" ? frontmatter.phaseCountAtGeneration : 0;
|
|
122
|
+
if (!generatedAt)
|
|
123
|
+
return undefined;
|
|
124
|
+
return { content: body.trim(), generatedAt, phaseCountAtGeneration };
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function splitFrontmatter(raw) {
|
|
131
|
+
if (!raw.startsWith("---\n"))
|
|
132
|
+
return { frontmatter: null, body: raw };
|
|
133
|
+
const end = raw.indexOf("\n---\n", 4);
|
|
134
|
+
if (end === -1)
|
|
135
|
+
return { frontmatter: null, body: raw };
|
|
136
|
+
const yamlText = raw.slice(4, end);
|
|
137
|
+
const body = raw.slice(end + 5);
|
|
138
|
+
try {
|
|
139
|
+
const parsed = parseSimpleYaml(yamlText);
|
|
140
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
141
|
+
return { frontmatter: parsed, body };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// fall through
|
|
146
|
+
}
|
|
147
|
+
return { frontmatter: null, body };
|
|
148
|
+
}
|
|
@@ -1,36 +1,15 @@
|
|
|
1
1
|
import { cp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { basename, join, resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { autoCompletePhase, buildAutoSummary, isPhaseRunning, runAuto, runSinglePhase } from "./auto-runner.js";
|
|
4
|
+
import { disposeAgentsWidget } from "./agent-widget.js";
|
|
5
|
+
import { parseDashboardFlags } from "./dashboard-flags.js";
|
|
6
|
+
import { narrateDashboard } from "./dashboard-narrator.js";
|
|
7
|
+
import { writeDashboard } from "./dashboard-writer.js";
|
|
8
8
|
import { parseNextFlags } from "./next-flags.js";
|
|
9
|
-
import {
|
|
9
|
+
import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPath, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, PIPELINE_ALIASES, resolvePhase, resolvePipelineChoice, stringifySimpleYaml, validatePhaseOutput, } from "../../core/index.js";
|
|
10
10
|
const STATUS_WIDGET_ID = "codecarto-widget";
|
|
11
11
|
const STATUS_LINE_ID = "codecarto-status";
|
|
12
12
|
const SAFE_TOOL_NAMES = ["read", "grep", "find", "ls", "edit", "write"];
|
|
13
|
-
async function recordUsage(workspaceDir, phaseId, status, activity) {
|
|
14
|
-
try {
|
|
15
|
-
await appendUsageRun(workspaceDir, {
|
|
16
|
-
timestamp: new Date().toISOString(),
|
|
17
|
-
phase: phaseId,
|
|
18
|
-
status,
|
|
19
|
-
turn_count: activity.turnCount,
|
|
20
|
-
tool_uses: activity.toolUses,
|
|
21
|
-
duration_ms: (activity.completedAt ?? Date.now()) - activity.startedAt,
|
|
22
|
-
tokens: {
|
|
23
|
-
input: activity.lifetimeUsage.input,
|
|
24
|
-
output: activity.lifetimeUsage.output,
|
|
25
|
-
cache_write: activity.lifetimeUsage.cacheWrite,
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
// Local usage logging is best-effort. A full disk or permission
|
|
31
|
-
// failure shouldn't surface as a phase error to the user.
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
13
|
function formatUsageTokens(count) {
|
|
35
14
|
if (count >= 1_000_000)
|
|
36
15
|
return `${(count / 1_000_000).toFixed(2)}M`;
|
|
@@ -203,6 +182,9 @@ export default function codeCartographerExtension(pi) {
|
|
|
203
182
|
lastFeedbackLines = [`Initialized workspace with pipeline: ${getPipelineLabel(selectedPipelinePath)}`];
|
|
204
183
|
ctx.ui.notify(`Initialized CodeCartographer (${getPipelineLabel(selectedPipelinePath)})`, "info");
|
|
205
184
|
await ctx.reload();
|
|
185
|
+
// Render the initial dashboard (empty usage, all phases pending) so
|
|
186
|
+
// the user sees the file exist immediately after /codecarto-init.
|
|
187
|
+
void writeDashboard(ctx.cwd, PACKAGE_VERSION);
|
|
206
188
|
return;
|
|
207
189
|
},
|
|
208
190
|
});
|
|
@@ -219,15 +201,19 @@ export default function codeCartographerExtension(pi) {
|
|
|
219
201
|
},
|
|
220
202
|
});
|
|
221
203
|
pi.registerCommand("codecarto-next", {
|
|
222
|
-
description: "Run the next eligible CodeCartographer phase as a sub-agent. Flags: --llm-steer / --no-llm-steer",
|
|
204
|
+
description: "Run the next eligible CodeCartographer phase as a sub-agent. Flags: --llm-steer / --no-llm-steer / --auto [--strict]",
|
|
223
205
|
getArgumentCompletions: (prefix) => {
|
|
224
|
-
const items = ["--llm-steer", "--no-llm-steer"]
|
|
206
|
+
const items = ["--llm-steer", "--no-llm-steer", "--auto", "--strict"]
|
|
225
207
|
.filter((value) => value.startsWith(prefix))
|
|
226
208
|
.map((value) => ({ value, label: value }));
|
|
227
209
|
return items.length > 0 ? items : null;
|
|
228
210
|
},
|
|
229
211
|
handler: async (args, ctx) => {
|
|
230
212
|
const flags = parseNextFlags(args);
|
|
213
|
+
if (flags.error) {
|
|
214
|
+
ctx.ui.notify(flags.error, "error");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
231
217
|
if (flags.unknown.length > 0) {
|
|
232
218
|
ctx.ui.notify(`Unknown /codecarto-next flag: ${flags.unknown.join(" ")}`, "error");
|
|
233
219
|
return;
|
|
@@ -235,6 +221,33 @@ export default function codeCartographerExtension(pi) {
|
|
|
235
221
|
const state = await ensureWorkspaceState(ctx);
|
|
236
222
|
if (!state)
|
|
237
223
|
return;
|
|
224
|
+
if (flags.auto) {
|
|
225
|
+
ctx.ui.notify(`Auto pipeline${flags.strict ? " (strict)" : ""} running…`, "info");
|
|
226
|
+
const result = await runAuto(ctx, pi, state, {
|
|
227
|
+
strict: flags.strict,
|
|
228
|
+
llmSteerOverride: flags.llmSteerOverride,
|
|
229
|
+
signal: ctx.signal,
|
|
230
|
+
onPhaseAdvanced: (advancedState) => {
|
|
231
|
+
// Refresh the status widget + session name between phases so
|
|
232
|
+
// the readout tracks progress live instead of staying frozen
|
|
233
|
+
// at the initial phase until the whole auto run finishes.
|
|
234
|
+
setUiState(ctx, advancedState, [`Auto pipeline${flags.strict ? " (strict)" : ""} running…`]);
|
|
235
|
+
const phaseId = getNextEligiblePhase(advancedState)?.id ?? advancedState.status.current_phase;
|
|
236
|
+
if (phaseId)
|
|
237
|
+
pi.setSessionName(`CodeCartographer: ${phaseId}`);
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
const availableSkills = await listSkillNames(state.workspaceDir).catch(() => []);
|
|
241
|
+
pi.sendMessage({
|
|
242
|
+
customType: "codecarto-auto-summary",
|
|
243
|
+
content: buildAutoSummary(result, availableSkills),
|
|
244
|
+
display: true,
|
|
245
|
+
});
|
|
246
|
+
lastFeedbackLines = [`Auto pipeline ${result.outcome}: ${result.reason}`];
|
|
247
|
+
await refreshWorkspaceUi(ctx, lastFeedbackLines);
|
|
248
|
+
ctx.ui.notify(`Auto pipeline ${result.outcome}: ${result.phasesRun.length}/${result.totalPhases} phases.`, result.outcome === "complete" ? "info" : "warning");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
238
251
|
const phase = getNextEligiblePhase(state);
|
|
239
252
|
if (!phase) {
|
|
240
253
|
lastFeedbackLines = ["All phases complete."];
|
|
@@ -244,122 +257,23 @@ export default function codeCartographerExtension(pi) {
|
|
|
244
257
|
}
|
|
245
258
|
// Reject re-entry: don't spawn a duplicate runner for a phase that's
|
|
246
259
|
// already in flight from a previous /codecarto-next invocation.
|
|
247
|
-
|
|
248
|
-
if (existing && existing.status === "running") {
|
|
260
|
+
if (isPhaseRunning(phase.id)) {
|
|
249
261
|
ctx.ui.notify(`Phase ${phase.id} is already running.`, "warning");
|
|
250
262
|
return;
|
|
251
263
|
}
|
|
252
264
|
const config = await loadCodecartoConfig(state.workspaceDir);
|
|
253
265
|
const llmSteerEnabled = flags.llmSteerOverride ?? config.orchestrator.llm_steer_next_phase;
|
|
254
|
-
let prompt = await buildPhasePrompt(state, phase, false);
|
|
255
|
-
if (llmSteerEnabled) {
|
|
256
|
-
ctx.ui.notify(`Customizing ${phase.id} prompt via LLM rewriter…`, "info");
|
|
257
|
-
const rewrite = await rewritePhasePrompt({ ctx, state, originalPrompt: prompt, nextPhaseId: phase.id });
|
|
258
|
-
if (rewrite.used) {
|
|
259
|
-
prompt = rewrite.prompt;
|
|
260
|
-
ctx.ui.notify(`LLM rewriter customized ${phase.id} seed prompt.`, "info");
|
|
261
|
-
// Inject the full rewritten prompt into the orchestrator's session
|
|
262
|
-
// so the user can audit what the rewriter chose to emphasize before
|
|
263
|
-
// the phase sub-agent starts. Same pattern as the phase-completion
|
|
264
|
-
// summary: display:true renders in the TUI; no triggerTurn so the
|
|
265
|
-
// orchestrator doesn't auto-respond.
|
|
266
|
-
pi.sendMessage({
|
|
267
|
-
customType: "codecarto-steering",
|
|
268
|
-
content: buildSteeringMessage({
|
|
269
|
-
nextPhaseId: phase.id,
|
|
270
|
-
prevPhaseId: rewrite.prevPhaseId,
|
|
271
|
-
rewrittenPrompt: rewrite.prompt,
|
|
272
|
-
}),
|
|
273
|
-
display: true,
|
|
274
|
-
});
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
ctx.ui.notify(`LLM rewriter skipped (${rewrite.skipReason}); using stock prompt.`, "warning");
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
const activity = startPhase(phase.id);
|
|
281
266
|
lastFeedbackLines = [`Running ${phase.id} phase as sub-agent`];
|
|
282
267
|
setUiState(ctx, state, lastFeedbackLines);
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
// phase
|
|
286
|
-
|
|
287
|
-
getAgentsWidget().attach(ctx.ui);
|
|
288
|
-
// Fire-and-forget: spawn the phase runner asynchronously so the
|
|
289
|
-
// orchestrator's TUI stays responsive. The runner mutates the shared
|
|
290
|
-
// agent-state map from event callbacks; M2 will read that map from a
|
|
291
|
-
// persistent widget. For M1 we just notify on completion.
|
|
292
|
-
void runPhase(ctx, prompt, {
|
|
293
|
-
onSessionCreated: (session) => { activity.session = session; },
|
|
294
|
-
onToolStart: (id, name) => { activity.activeTools.set(id, name); activity.toolUses++; },
|
|
295
|
-
onToolEnd: (id) => { activity.activeTools.delete(id); },
|
|
296
|
-
onTextDelta: (_delta, fullText) => { activity.responseText = fullText; },
|
|
297
|
-
onTurnEnd: (turnCount) => { activity.turnCount = turnCount; },
|
|
298
|
-
onMessageEnd: (usage) => {
|
|
299
|
-
activity.lifetimeUsage.input += usage.input;
|
|
300
|
-
activity.lifetimeUsage.output += usage.output;
|
|
301
|
-
activity.lifetimeUsage.cacheWrite += usage.cacheWrite;
|
|
302
|
-
},
|
|
303
|
-
}, { sessionName: `CodeCartographer phase: ${phase.id}` })
|
|
304
|
-
.then((result) => {
|
|
305
|
-
const status = result.aborted ? "aborted" : "completed";
|
|
306
|
-
finishPhase(phase.id, { status });
|
|
307
|
-
if (ctx.hasUI) {
|
|
308
|
-
ctx.ui.notify(result.aborted
|
|
309
|
-
? `Phase ${phase.id} aborted.`
|
|
310
|
-
: `Phase ${phase.id} sub-agent finished (${result.toolUses} tool uses, ${result.turnCount} turns).`, result.aborted ? "warning" : "info");
|
|
311
|
-
}
|
|
312
|
-
// Inject a CustomMessageEntry into the orchestrator's session so
|
|
313
|
-
// the user sees a closeout summary in the TUI scrollback and the
|
|
314
|
-
// orchestrator's LLM picks up the phase result as context on the
|
|
315
|
-
// next turn. display:true renders in the TUI; no triggerTurn so
|
|
316
|
-
// the LLM doesn't auto-respond — the user remains in control.
|
|
317
|
-
pi.sendMessage({
|
|
318
|
-
customType: "codecarto-phase-summary",
|
|
319
|
-
content: buildPhaseSummary({
|
|
320
|
-
phaseId: phase.id,
|
|
321
|
-
status: result.aborted ? "aborted" : "completed",
|
|
322
|
-
turnCount: activity.turnCount,
|
|
323
|
-
toolUses: activity.toolUses,
|
|
324
|
-
tokens: activity.lifetimeUsage,
|
|
325
|
-
durationMs: (activity.completedAt ?? Date.now()) - activity.startedAt,
|
|
326
|
-
responseText: result.responseText,
|
|
327
|
-
}),
|
|
328
|
-
display: true,
|
|
329
|
-
});
|
|
330
|
-
void recordUsage(state.workspaceDir, phase.id, status, activity);
|
|
331
|
-
})
|
|
332
|
-
.catch((err) => {
|
|
333
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
334
|
-
finishPhase(phase.id, { status: "error", error: message });
|
|
335
|
-
if (ctx.hasUI) {
|
|
336
|
-
ctx.ui.notify(`Phase ${phase.id} sub-agent failed: ${message}`, "error");
|
|
337
|
-
}
|
|
338
|
-
pi.sendMessage({
|
|
339
|
-
customType: "codecarto-phase-summary",
|
|
340
|
-
content: buildPhaseSummary({
|
|
341
|
-
phaseId: phase.id,
|
|
342
|
-
status: "error",
|
|
343
|
-
turnCount: activity.turnCount,
|
|
344
|
-
toolUses: activity.toolUses,
|
|
345
|
-
tokens: activity.lifetimeUsage,
|
|
346
|
-
durationMs: (activity.completedAt ?? Date.now()) - activity.startedAt,
|
|
347
|
-
responseText: "",
|
|
348
|
-
error: message,
|
|
349
|
-
}),
|
|
350
|
-
display: true,
|
|
351
|
-
});
|
|
352
|
-
void recordUsage(state.workspaceDir, phase.id, "error", activity);
|
|
353
|
-
})
|
|
268
|
+
// Fire-and-forget: keep the TUI responsive while the sub-agent works.
|
|
269
|
+
// runSinglePhase handles all side effects (steering message, notify,
|
|
270
|
+
// phase summary, recordUsage, dashboard regen, clearPhase linger).
|
|
271
|
+
void runSinglePhase(ctx, pi, state, phase, { llmSteerEnabled, signal: ctx.signal })
|
|
354
272
|
.finally(() => {
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
// state without waiting for the user to run /codecarto-status.
|
|
273
|
+
// Refresh the status widget after the phase resolves so the
|
|
274
|
+
// "Open questions / Carry-forward / Next" lines reflect any
|
|
275
|
+
// owner_notes the sub-agent wrote to status.yaml.
|
|
359
276
|
void refreshWorkspaceUi(ctx);
|
|
360
|
-
// Linger 30s in M1 so /codecarto-status can show that the phase ran;
|
|
361
|
-
// M2's widget owns the proper "linger N turns" lifecycle.
|
|
362
|
-
setTimeout(() => clearPhase(phase.id), 30_000);
|
|
363
277
|
});
|
|
364
278
|
},
|
|
365
279
|
});
|
|
@@ -397,7 +311,13 @@ export default function codeCartographerExtension(pi) {
|
|
|
397
311
|
const state = await ensureWorkspaceState(ctx);
|
|
398
312
|
if (!state)
|
|
399
313
|
return;
|
|
400
|
-
const validation = await validatePhaseOutput(state, args.trim() || undefined);
|
|
314
|
+
const validation = await validatePhaseOutput(state, args.trim() || undefined).catch((error) => error instanceof Error ? error : new Error(String(error)));
|
|
315
|
+
if (validation instanceof Error) {
|
|
316
|
+
lastFeedbackLines = [validation.message];
|
|
317
|
+
setUiState(ctx, state, lastFeedbackLines);
|
|
318
|
+
ctx.ui.notify(validation.message, "error");
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
401
321
|
lastFeedbackLines = buildValidationSummary(validation);
|
|
402
322
|
setUiState(ctx, state, lastFeedbackLines);
|
|
403
323
|
const level = validation.overall === "FAIL" || validation.overall === "MISSING" ? "error" : validation.overall === "PASS WITH GAPS" ? "warning" : "info";
|
|
@@ -410,83 +330,20 @@ export default function codeCartographerExtension(pi) {
|
|
|
410
330
|
const currentState = await ensureWorkspaceState(ctx);
|
|
411
331
|
if (!currentState)
|
|
412
332
|
return;
|
|
413
|
-
const validation = await validatePhaseOutput(currentState, args.trim() || undefined);
|
|
333
|
+
const validation = await validatePhaseOutput(currentState, args.trim() || undefined).catch((error) => error instanceof Error ? error : new Error(String(error)));
|
|
334
|
+
if (validation instanceof Error) {
|
|
335
|
+
lastFeedbackLines = [validation.message];
|
|
336
|
+
setUiState(ctx, currentState, lastFeedbackLines);
|
|
337
|
+
ctx.ui.notify(validation.message, "error");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
414
340
|
if (validation.overall === "FAIL" || validation.overall === "MISSING") {
|
|
415
341
|
lastFeedbackLines = buildValidationSummary(validation);
|
|
416
342
|
setUiState(ctx, currentState, lastFeedbackLines);
|
|
417
343
|
ctx.ui.notify(`Cannot complete ${validation.phaseId}: ${validation.overall}`, "error");
|
|
418
344
|
return;
|
|
419
345
|
}
|
|
420
|
-
const
|
|
421
|
-
const updatedState = await updateStatusAtomically(ctx.cwd, (lockedState) => {
|
|
422
|
-
const phase = resolvePhase(lockedState, validation.phaseId);
|
|
423
|
-
if (!phase?.primary_output) {
|
|
424
|
-
throw new Error(`Phase ${validation.phaseId} is missing primary_output.`);
|
|
425
|
-
}
|
|
426
|
-
const nextStatus = normalizeStatus(lockedState.status, lockedState.pipeline, lockedState.status.pipeline, lockedState.cwd);
|
|
427
|
-
const existingPhase = nextStatus.phases[validation.phaseId] ?? {
|
|
428
|
-
status: "pending",
|
|
429
|
-
owner_notes: [],
|
|
430
|
-
outputs_present: [],
|
|
431
|
-
open_questions: [],
|
|
432
|
-
carry_forward: [],
|
|
433
|
-
};
|
|
434
|
-
const gapEntries = validation.rows
|
|
435
|
-
.filter((row) => row.result.toUpperCase().includes("PARTIAL"))
|
|
436
|
-
.map((row) => ({
|
|
437
|
-
kind: "needs-maintainer-decision",
|
|
438
|
-
description: row.criterion || "Partial validation gap",
|
|
439
|
-
deferred_reason: row.evidence || "Marked PARTIAL by validation",
|
|
440
|
-
}));
|
|
441
|
-
const mergedOpenQuestions = [...existingPhase.open_questions];
|
|
442
|
-
for (const candidate of gapEntries) {
|
|
443
|
-
const dupe = mergedOpenQuestions.some((entry) => entry.description === candidate.description && entry.deferred_reason === candidate.deferred_reason);
|
|
444
|
-
if (!dupe)
|
|
445
|
-
mergedOpenQuestions.push(candidate);
|
|
446
|
-
}
|
|
447
|
-
nextStatus.phases[validation.phaseId] = {
|
|
448
|
-
status: "complete",
|
|
449
|
-
owner_notes: uniqueStrings([
|
|
450
|
-
...existingPhase.owner_notes,
|
|
451
|
-
`Completed via /codecarto-complete on ${completionTimestamp}.`,
|
|
452
|
-
`Primary output: .codecarto/${validation.primaryOutput}`,
|
|
453
|
-
`Validation: ${validation.overall}`,
|
|
454
|
-
]).slice(-3),
|
|
455
|
-
outputs_present: uniqueStrings([...existingPhase.outputs_present, validation.primaryOutput]),
|
|
456
|
-
open_questions: mergedOpenQuestions,
|
|
457
|
-
carry_forward: existingPhase.carry_forward ?? [],
|
|
458
|
-
};
|
|
459
|
-
nextStatus.last_updated = completionTimestamp;
|
|
460
|
-
const updatedWorkspaceState = {
|
|
461
|
-
...lockedState,
|
|
462
|
-
status: nextStatus,
|
|
463
|
-
};
|
|
464
|
-
const nextEligible = getNextEligiblePhase(updatedWorkspaceState);
|
|
465
|
-
nextStatus.current_phase = nextEligible?.id ?? "complete";
|
|
466
|
-
nextStatus.next_actions = nextEligible
|
|
467
|
-
? [
|
|
468
|
-
`Begin ${nextEligible.id} phase by producing ${nextEligible.primary_output ?? `findings/${nextEligible.id}/`}`,
|
|
469
|
-
]
|
|
470
|
-
: ["All phases complete. Review findings, open questions, and downstream implementation notes."];
|
|
471
|
-
return {
|
|
472
|
-
state: {
|
|
473
|
-
...updatedWorkspaceState,
|
|
474
|
-
status: nextStatus,
|
|
475
|
-
},
|
|
476
|
-
threadLogEntry: buildThreadLogEntry(validation.phaseId, validation, completionTimestamp),
|
|
477
|
-
};
|
|
478
|
-
});
|
|
479
|
-
let closeoutNotice;
|
|
480
|
-
try {
|
|
481
|
-
const created = await ensureCloseoutStub(updatedState.workspaceDir, validation.phaseId, completionTimestamp);
|
|
482
|
-
if (created) {
|
|
483
|
-
closeoutNotice = `Closeout stub: .codecarto/closeouts/${closeoutFileName(dateOnly(completionTimestamp), validation.phaseId)} (fill it in)`;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
catch (error) {
|
|
487
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
488
|
-
closeoutNotice = `Closeout stub not created: ${message}`;
|
|
489
|
-
}
|
|
346
|
+
const { updatedState, closeoutNotice } = await autoCompletePhase(ctx, validation);
|
|
490
347
|
lastFeedbackLines = [
|
|
491
348
|
`Completed phase: ${validation.phaseId}`,
|
|
492
349
|
`Validation: ${validation.overall}`,
|
|
@@ -567,4 +424,37 @@ export default function codeCartographerExtension(pi) {
|
|
|
567
424
|
ctx.ui.notify(`CodeCartographer usage: ${totals.runs} run${totals.runs === 1 ? "" : "s"}, ${formatUsageTokens(totals.tokens.input + totals.tokens.output)} tokens total`, "info");
|
|
568
425
|
},
|
|
569
426
|
});
|
|
427
|
+
pi.registerCommand("codecarto-dashboard", {
|
|
428
|
+
description: "Regenerate .codecarto/dashboard.html (use --narrate for an LLM executive summary)",
|
|
429
|
+
getArgumentCompletions: (prefix) => {
|
|
430
|
+
const items = ["--narrate"]
|
|
431
|
+
.filter((value) => value.startsWith(prefix))
|
|
432
|
+
.map((value) => ({ value, label: value }));
|
|
433
|
+
return items.length > 0 ? items : null;
|
|
434
|
+
},
|
|
435
|
+
handler: async (args, ctx) => {
|
|
436
|
+
const flags = parseDashboardFlags(args);
|
|
437
|
+
if (flags.unknown.length > 0) {
|
|
438
|
+
ctx.ui.notify(`Unknown /codecarto-dashboard flag: ${flags.unknown.join(" ")}`, "error");
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const state = await ensureWorkspaceState(ctx);
|
|
442
|
+
if (!state)
|
|
443
|
+
return;
|
|
444
|
+
if (flags.narrate) {
|
|
445
|
+
ctx.ui.notify(`Narrating dashboard via LLM…`, "info");
|
|
446
|
+
const result = await narrateDashboard(ctx, state);
|
|
447
|
+
if (result.used) {
|
|
448
|
+
ctx.ui.notify("Narration written to .codecarto/.dashboard-narration.local.md", "info");
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
ctx.ui.notify(`LLM narration skipped (${result.skipReason}); rendering deterministic dashboard.`, "warning");
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
await writeDashboard(ctx.cwd, PACKAGE_VERSION);
|
|
455
|
+
lastFeedbackLines = ["Dashboard regenerated: .codecarto/dashboard.html"];
|
|
456
|
+
setUiState(ctx, state, lastFeedbackLines);
|
|
457
|
+
ctx.ui.notify("Dashboard regenerated: .codecarto/dashboard.html", "info");
|
|
458
|
+
},
|
|
459
|
+
});
|
|
570
460
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export interface NextFlags {
|
|
2
2
|
llmSteerOverride?: boolean;
|
|
3
|
+
auto: boolean;
|
|
4
|
+
strict: boolean;
|
|
3
5
|
unknown: string[];
|
|
6
|
+
/** Set when --strict is passed without --auto. Caller surfaces as an error. */
|
|
7
|
+
error?: string;
|
|
4
8
|
}
|
|
5
9
|
export declare function parseNextFlags(args: string): NextFlags;
|
|
6
10
|
export declare const KNOWN_NEXT_FLAGS: readonly string[];
|
|
@@ -1,19 +1,32 @@
|
|
|
1
|
-
// Flag parser for /codecarto-next.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
1
|
+
// Flag parser for /codecarto-next. Recognized flags:
|
|
2
|
+
// --llm-steer / --no-llm-steer — override the workspace config's
|
|
3
|
+
// llm_steer_next_phase per invocation.
|
|
4
|
+
// --auto — run the entire pipeline end-to-end.
|
|
5
|
+
// --strict — only with --auto; stop on PASS WITH GAPS
|
|
6
|
+
// instead of auto-advancing.
|
|
7
|
+
//
|
|
8
|
+
// The parser returns a populated NextFlags shape and never throws. index.ts
|
|
9
|
+
// decides how to surface errors (unknown flags + invalid combinations) and
|
|
10
|
+
// how to compose --llm-steer with workspace config.
|
|
11
|
+
const KNOWN = new Set(["--llm-steer", "--no-llm-steer", "--auto", "--strict"]);
|
|
6
12
|
export function parseNextFlags(args) {
|
|
7
13
|
const tokens = args.trim().split(/\s+/).filter((t) => t.length > 0);
|
|
8
|
-
const result = { unknown: [] };
|
|
14
|
+
const result = { auto: false, strict: false, unknown: [] };
|
|
9
15
|
for (const t of tokens) {
|
|
10
16
|
if (t === "--llm-steer")
|
|
11
17
|
result.llmSteerOverride = true;
|
|
12
18
|
else if (t === "--no-llm-steer")
|
|
13
19
|
result.llmSteerOverride = false;
|
|
20
|
+
else if (t === "--auto")
|
|
21
|
+
result.auto = true;
|
|
22
|
+
else if (t === "--strict")
|
|
23
|
+
result.strict = true;
|
|
14
24
|
else
|
|
15
25
|
result.unknown.push(t);
|
|
16
26
|
}
|
|
27
|
+
if (result.strict && !result.auto) {
|
|
28
|
+
result.error = "Flag --strict requires --auto.";
|
|
29
|
+
}
|
|
17
30
|
return result;
|
|
18
31
|
}
|
|
19
32
|
export const KNOWN_NEXT_FLAGS = [...KNOWN];
|
|
@@ -68,6 +68,27 @@ export declare function handleSkill(args: {
|
|
|
68
68
|
}>;
|
|
69
69
|
structuredContent?: Record<string, unknown>;
|
|
70
70
|
}>;
|
|
71
|
+
export declare function handlePublish(args: Record<string, unknown>): Promise<{
|
|
72
|
+
content: Array<{
|
|
73
|
+
type: "text";
|
|
74
|
+
text: string;
|
|
75
|
+
}>;
|
|
76
|
+
structuredContent?: Record<string, unknown>;
|
|
77
|
+
}>;
|
|
78
|
+
export declare function handleLibraryList(args: Record<string, unknown>): Promise<{
|
|
79
|
+
content: Array<{
|
|
80
|
+
type: "text";
|
|
81
|
+
text: string;
|
|
82
|
+
}>;
|
|
83
|
+
structuredContent?: Record<string, unknown>;
|
|
84
|
+
}>;
|
|
85
|
+
export declare function handleLibraryReindex(args: Record<string, unknown>): Promise<{
|
|
86
|
+
content: Array<{
|
|
87
|
+
type: "text";
|
|
88
|
+
text: string;
|
|
89
|
+
}>;
|
|
90
|
+
structuredContent?: Record<string, unknown>;
|
|
91
|
+
}>;
|
|
71
92
|
export declare function buildServer(): Server<{
|
|
72
93
|
method: string;
|
|
73
94
|
params?: {
|