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.
@@ -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 { runPhase } from "./agent-runner.js";
4
- import { buildSteeringMessage, rewritePhasePrompt } from "./agent-rewriter.js";
5
- import { clearPhase, finishPhase, getPhaseActivity, startPhase } from "./agent-state.js";
6
- import { buildPhaseSummary } from "./agent-summary.js";
7
- import { disposeAgentsWidget, getAgentsWidget } from "./agent-widget.js";
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 { appendUsageRun, buildPhasePrompt, buildSkillPrompt, buildThreadLogEntry, buildValidationSummary, canonicalPath, closeoutFileName, computePerPhaseTotals, computeTotals, createEmptyStatus, dateOnly, DEFAULT_PIPELINE_PATH, ensureCloseoutStub, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPath, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, normalizeStatus, packagedWorkspaceDir, pathExists, PIPELINE_ALIASES, resolvePhase, resolvePipelineChoice, stringifySimpleYaml, uniqueStrings, updateStatusAtomically, validatePhaseOutput, } from "../../core/index.js";
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
- const existing = getPhaseActivity(phase.id);
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
- ctx.ui.notify(`CodeCartographer phase: ${phase.id} (sub-agent running)`, "info");
284
- // Attach the persistent "Agents" widget so the user can watch the
285
- // phase's tool/turn/token counts live above the editor while the
286
- // orchestrator's TUI stays responsive.
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
- // Sub-agent may have written findings, owner_notes, or carry-forward
356
- // items into status.yaml. Refresh the main status widget so the
357
- // "Open questions / Carry-forward / Next" lines reflect the new
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 completionTimestamp = new Date().toISOString();
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. The user invokes the slash command with
2
- // an optional "--llm-steer" or "--no-llm-steer" override. We parse and
3
- // validate; index.ts decides how to merge the override with workspace
4
- // config to compute the effective "should we run the LLM rewriter?" bit.
5
- const KNOWN = new Set(["--llm-steer", "--no-llm-steer"]);
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?: {