codecartographer-pi 0.19.6 → 0.21.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.
Files changed (57) hide show
  1. package/.codecarto/GUIDE.md +2 -2
  2. package/.codecarto/broadside/SKILL.md +21 -3
  3. package/.codecarto/broadside/config.yaml +35 -9
  4. package/.codecarto/findings/contracts/SKILL.md +4 -1
  5. package/.codecarto/findings/defect-scan/SKILL.md +10 -0
  6. package/.codecarto/findings/defect-scan-mechanical/SKILL.md +6 -0
  7. package/.codecarto/findings/defect-scan-semantic/SKILL.md +8 -1
  8. package/.codecarto/findings/porting/SKILL.md +4 -0
  9. package/.codecarto/findings/protocols/SKILL.md +4 -0
  10. package/.codecarto/templates/mechanical-defects.md +15 -0
  11. package/.codecarto/templates/reimplementation-spec.md +5 -3
  12. package/.codecarto/templates/reverse-engineering-bundle.md +10 -1
  13. package/.codecarto/templates/semantic-defects.md +15 -0
  14. package/.codecarto/workflow/VALIDATE.md +3 -2
  15. package/.codecarto/workflow/scaffold-version.yaml +1 -1
  16. package/README.md +13 -9
  17. package/dist/core/amendment.js +9 -4
  18. package/dist/core/broadside.d.ts +121 -2
  19. package/dist/core/broadside.js +478 -92
  20. package/dist/core/completion.js +88 -23
  21. package/dist/core/dashboard-writer.d.ts +8 -0
  22. package/dist/core/dashboard-writer.js +159 -0
  23. package/dist/core/index.d.ts +2 -0
  24. package/dist/core/index.js +2 -0
  25. package/dist/core/library.js +9 -4
  26. package/dist/core/orchestrator-config.d.ts +32 -7
  27. package/dist/core/orchestrator-config.js +124 -44
  28. package/dist/core/pipeline.d.ts +73 -0
  29. package/dist/core/pipeline.js +134 -10
  30. package/dist/core/prompts.d.ts +20 -0
  31. package/dist/core/prompts.js +53 -13
  32. package/dist/core/secrets.d.ts +16 -0
  33. package/dist/core/secrets.js +98 -0
  34. package/dist/core/status.d.ts +16 -0
  35. package/dist/core/status.js +47 -20
  36. package/dist/core/synthesis.js +5 -2
  37. package/dist/core/utils.d.ts +7 -0
  38. package/dist/core/utils.js +7 -0
  39. package/dist/core/workspace.d.ts +55 -8
  40. package/dist/core/workspace.js +116 -8
  41. package/dist/core/yaml.js +181 -15
  42. package/dist/extensions/codecarto/agent-rewriter.js +21 -14
  43. package/dist/extensions/codecarto/agent-runner.d.ts +6 -2
  44. package/dist/extensions/codecarto/agent-runner.js +27 -9
  45. package/dist/extensions/codecarto/agent-state.d.ts +0 -2
  46. package/dist/extensions/codecarto/auto-runner.d.ts +1 -1
  47. package/dist/extensions/codecarto/auto-runner.js +27 -8
  48. package/dist/extensions/codecarto/broadside-flags.d.ts +3 -1
  49. package/dist/extensions/codecarto/broadside-flags.js +12 -0
  50. package/dist/extensions/codecarto/dashboard-narrator.js +9 -2
  51. package/dist/extensions/codecarto/dashboard-writer.d.ts +1 -8
  52. package/dist/extensions/codecarto/dashboard-writer.js +5 -154
  53. package/dist/extensions/codecarto/index.js +158 -47
  54. package/dist/extensions/codecarto/phase-compaction.js +5 -1
  55. package/dist/mcp-server/server.d.ts +3 -1
  56. package/dist/mcp-server/server.js +205 -77
  57. package/package.json +3 -2
@@ -17,8 +17,7 @@ import { clearPhase, finishPhase, getPhaseActivity, startPhase } from "./agent-s
17
17
  import { buildSteeringMessage, rewritePhasePrompt } from "./agent-rewriter.js";
18
18
  import { buildPhaseSummary } from "./agent-summary.js";
19
19
  import { getAgentsWidget } from "./agent-widget.js";
20
- import { writeDashboard } from "./dashboard-writer.js";
21
- import { appendUsageRun, buildPhasePrompt, buildValidationSummary, completeValidatedPhase, formatMillis, formatTokenCount, getNextEligiblePhase, getWorkspaceState, loadCodecartoConfig, PACKAGE_VERSION, PhasePreflightError, runPhasePreflight, validatePhaseOutput, } from "../../core/index.js";
20
+ import { appendUsageRun, buildPhasePrompt, buildValidationSummary, completeValidatedPhase, formatMillis, formatTokenCount, describeStuckPipeline, resolvePipelineOutcome, getWorkspaceState, describeConfigProblems, loadCodecartoConfig, PACKAGE_VERSION, PhasePreflightError, runPhasePreflight, validatePhaseOutput, writeDashboard, } from "../../core/index.js";
22
21
  /**
23
22
  * Run one phase end to end: optional LLM-steered rewrite, spawn the sub-agent,
24
23
  * wait for it, then emit the side effects the historical /codecarto-next chain
@@ -60,7 +59,6 @@ export async function runSinglePhase(ctx, pi, state, phase, options) {
60
59
  getAgentsWidget().attach(ctx.ui);
61
60
  try {
62
61
  const result = await runPhase(ctx, prompt, {
63
- onSessionCreated: (session) => { activity.session = session; },
64
62
  onToolStart: (id, name) => { activity.activeTools.set(id, name); activity.toolUses++; },
65
63
  onToolEnd: (id) => { activity.activeTools.delete(id); },
66
64
  onTextDelta: (_delta, fullText) => { activity.responseText = fullText; },
@@ -101,7 +99,9 @@ export async function runSinglePhase(ctx, pi, state, phase, options) {
101
99
  display: true,
102
100
  });
103
101
  void recordUsage(state.workspaceDir, phase.id, status, activity, result.sessionFile);
104
- void writeDashboard(ctx.cwd, PACKAGE_VERSION);
102
+ // The phase's sub-agent replaced the session, so reading ctx.cwd here
103
+ // would throw (#201); the state captured before the run has the root.
104
+ void writeDashboard(state.cwd, PACKAGE_VERSION);
105
105
  return {
106
106
  status: result.aborted ? "aborted" : "completed",
107
107
  activity,
@@ -128,7 +128,7 @@ export async function runSinglePhase(ctx, pi, state, phase, options) {
128
128
  display: true,
129
129
  });
130
130
  void recordUsage(state.workspaceDir, phase.id, "error", activity);
131
- void writeDashboard(ctx.cwd, PACKAGE_VERSION);
131
+ void writeDashboard(state.cwd, PACKAGE_VERSION);
132
132
  return { status: "error", activity, error: message };
133
133
  }
134
134
  finally {
@@ -190,6 +190,10 @@ export async function runAuto(ctx, pi, initialState, options) {
190
190
  const totalTokens = { input: 0, output: 0, cacheWrite: 0 };
191
191
  const totalPhases = initialState.pipeline.phase_order.length;
192
192
  const config = await loadCodecartoConfig(initialState.workspaceDir);
193
+ // Once, before the loop: a dropped config file changes what this run does
194
+ // (the steer toggle lives there) and nothing else in the loop reads it.
195
+ if (config.problems.length > 0)
196
+ notifyCtx(ctx, describeConfigProblems(config).join("\n"), "warning");
193
197
  const llmSteerEnabled = options.llmSteerOverride ?? config.orchestrator.llm_steer_next_phase;
194
198
  let state = initialState;
195
199
  while (true) {
@@ -199,13 +203,22 @@ export async function runAuto(ctx, pi, initialState, options) {
199
203
  reason: "User aborted the auto run.",
200
204
  });
201
205
  }
202
- const phase = getNextEligiblePhase(state);
203
- if (!phase) {
206
+ const outcome = resolvePipelineOutcome(state);
207
+ if (outcome.kind === "stuck") {
208
+ // Not "complete": the loop ending because nothing can run is the
209
+ // case that used to read as success (#228).
210
+ return finish({
211
+ outcome: "stuck",
212
+ reason: describeStuckPipeline(outcome.blocked),
213
+ });
214
+ }
215
+ if (outcome.kind === "complete") {
204
216
  return finish({
205
217
  outcome: "complete",
206
218
  reason: "Pipeline complete.",
207
219
  });
208
220
  }
221
+ const phase = outcome.phase;
209
222
  let preflight;
210
223
  try {
211
224
  preflight = await runPhasePreflight(state, phase);
@@ -241,7 +254,7 @@ export async function runAuto(ctx, pi, initialState, options) {
241
254
  if (phaseResult.status === "completed") {
242
255
  // State must be refreshed because the sub-agent may have written
243
256
  // findings to disk that the validator reads.
244
- const stateForValidation = (await getWorkspaceState(ctx.cwd)) ?? state;
257
+ const stateForValidation = (await getWorkspaceState(autoCwd)) ?? state;
245
258
  validation = await validatePhaseOutput(stateForValidation, phase.id);
246
259
  }
247
260
  const decision = decideAfterPhase(phaseResult.status, phaseResult.error, validation, options.strict);
@@ -304,6 +317,8 @@ export function buildAutoSummary(result, availableSkills = []) {
304
317
  return `**Auto pipeline stopped at \`${result.stoppedAt?.phaseId ?? "?"}\`.**`;
305
318
  case "aborted":
306
319
  return `**Auto pipeline aborted${result.stoppedAt?.phaseId ? ` during \`${result.stoppedAt.phaseId}\`` : ""}.**`;
320
+ case "stuck":
321
+ return `**Auto pipeline stuck: no phase can run.**`;
307
322
  }
308
323
  })();
309
324
  const statsLine = `_⟳ ${ranOf} · ${tokensStr} tokens · ${wallTime}_`;
@@ -317,6 +332,10 @@ export function buildAutoSummary(result, availableSkills = []) {
317
332
  lines.push("", result.reason);
318
333
  lines.push("", recoveryHint(result));
319
334
  }
335
+ if (result.outcome === "stuck") {
336
+ // The reason is the stuck sentence, which already says what to fix.
337
+ lines.push("", result.reason);
338
+ }
320
339
  if (result.outcome === "complete") {
321
340
  lines.push("", "Dashboard: `.codecarto/dashboard.html`");
322
341
  if (availableSkills.length > 0) {
@@ -16,11 +16,13 @@ export interface BroadsideFlags {
16
16
  /** Undefined means "use the repository's config default". */
17
17
  maxCost?: number;
18
18
  waitSeconds?: number;
19
+ /** For collect: the run to collect instead of the most recent (#268). */
20
+ runId?: string;
19
21
  benchmarks: boolean;
20
22
  unknown: string[];
21
23
  /** Set on an invalid combination. The caller surfaces it as an error. */
22
24
  error?: string;
23
25
  }
24
26
  /** Every token the completer offers, in the order it offers them. */
25
- export declare const KNOWN_BROADSIDE_TOKENS: readonly ["submit", "collect", "status", "models", "architecture", "api", "security", "defect", "conventions", "porting", "--incremental", "--no-incremental", "--max-cost=", "--wait=", "--no-synthesis", "--no-triage", "--no-retry-truncated", "--benchmarks"];
27
+ export declare const KNOWN_BROADSIDE_TOKENS: readonly ["submit", "collect", "status", "models", "architecture", "api", "security", "defect", "conventions", "porting", "--incremental", "--no-incremental", "--max-cost=", "--wait=", "--run=", "--no-synthesis", "--no-triage", "--no-retry-truncated", "--benchmarks"];
26
28
  export declare function parseBroadsideFlags(args: string): BroadsideFlags;
@@ -13,6 +13,7 @@
13
13
  // --no-incremental --no-triage
14
14
  // --max-cost=N --no-retry-truncated
15
15
  // --wait=SECONDS --benchmarks (models only)
16
+ // --run=ID (collect only: an older run, as listed by status)
16
17
  //
17
18
  // --incremental has a spelled-out negative because the value is tri-state:
18
19
  // absent defers to config.yaml, so a repository that set `incremental: true`
@@ -34,6 +35,7 @@ export const KNOWN_BROADSIDE_TOKENS = [
34
35
  "--no-incremental",
35
36
  "--max-cost=",
36
37
  "--wait=",
38
+ "--run=",
37
39
  "--no-synthesis",
38
40
  "--no-triage",
39
41
  "--no-retry-truncated",
@@ -106,6 +108,13 @@ export function parseBroadsideFlags(args) {
106
108
  result.waitSeconds = parseNumeric(token, "--wait", result);
107
109
  continue;
108
110
  }
111
+ if (token.startsWith("--run=")) {
112
+ const value = token.slice("--run=".length).trim();
113
+ if (!value)
114
+ result.error ??= "--run= needs a run id (see /codecarto-broadside status).";
115
+ result.runId = value || undefined;
116
+ continue;
117
+ }
109
118
  result.unknown.push(token);
110
119
  }
111
120
  // Flags that only mean something for one action are refused rather than
@@ -125,5 +134,8 @@ export function parseBroadsideFlags(args) {
125
134
  if (result.action === "status" && result.waitSeconds !== undefined) {
126
135
  result.error ??= "--wait is only meaningful for submit and collect; status reads recorded state.";
127
136
  }
137
+ if (result.runId !== undefined && result.action !== "collect") {
138
+ result.error ??= `--run is only meaningful for collect (got action "${result.action}").`;
139
+ }
128
140
  return result;
129
141
  }
@@ -13,6 +13,7 @@
13
13
  import { readFile, readdir } from "node:fs/promises";
14
14
  import { join } from "node:path";
15
15
  import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
16
+ import { disposeChildSession } from "./agent-runner.js";
16
17
  import { atomicWriteFile, computeTotals, NARRATION_CACHE_RELATIVE_PATH, loadUsage, pathExists, stringifySimpleYaml, } from "../../core/index.js";
17
18
  import { createChildModelRuntime } from "./child-model-runtime.js";
18
19
  // Per-closeout byte budget when stuffing the narrator's input. Three
@@ -158,8 +159,14 @@ async function runNarratorOnce(ctx, prompt) {
158
159
  tools: [],
159
160
  resourceLoader: loader,
160
161
  });
161
- await session.prompt(prompt);
162
- return getLastAssistantText(session);
162
+ try {
163
+ await session.prompt(prompt);
164
+ return getLastAssistantText(session);
165
+ }
166
+ finally {
167
+ // One prompt, one answer; the child has nothing left to do (#256).
168
+ disposeChildSession(session);
169
+ }
163
170
  }
164
171
  function getLastAssistantText(session) {
165
172
  for (let i = session.messages.length - 1; i >= 0; i--) {
@@ -1,8 +1 @@
1
- /**
2
- * Render and atomically replace `.codecarto/dashboard.html`.
3
- * @returns true when a fresh dashboard landed on disk; false when the
4
- * workspace is missing or any gather/render/write step failed (swallowed —
5
- * lifecycle callers must never fail on a dashboard problem, but they may
6
- * report truthfully whether a refresh happened).
7
- */
8
- export declare function writeDashboard(cwd: string, packageVersion: string): Promise<boolean>;
1
+ export { writeDashboard } from "../../core/dashboard-writer.ts";
@@ -1,154 +1,5 @@
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 } from "node:fs/promises";
6
- import { join } from "node:path";
7
- import { atomicWriteFile, 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
- /**
10
- * Render and atomically replace `.codecarto/dashboard.html`.
11
- * @returns true when a fresh dashboard landed on disk; false when the
12
- * workspace is missing or any gather/render/write step failed (swallowed —
13
- * lifecycle callers must never fail on a dashboard problem, but they may
14
- * report truthfully whether a refresh happened).
15
- */
16
- export async function writeDashboard(cwd, packageVersion) {
17
- try {
18
- const state = await getWorkspaceState(cwd);
19
- if (!state)
20
- return false;
21
- const workspaceDir = state.workspaceDir;
22
- const [usage, closeouts, outputsPresent, narration] = await Promise.all([
23
- loadUsage(workspaceDir),
24
- listCloseouts(workspaceDir),
25
- buildOutputsPresent(state.workspaceDir, state.pipeline),
26
- loadNarration(workspaceDir),
27
- ]);
28
- const inputs = {
29
- status: state.status,
30
- pipeline: state.pipeline,
31
- usage,
32
- closeouts,
33
- outputsPresent,
34
- packageVersion,
35
- generatedAt: new Date().toISOString(),
36
- narration,
37
- };
38
- const html = renderDashboard(inputs);
39
- await atomicWriteFile(join(workspaceDir, DASHBOARD_RELATIVE_PATH), html);
40
- return true;
41
- }
42
- catch {
43
- // Best-effort: a failed dashboard write must not surface as a phase
44
- // error. The user's pipeline state is unaffected; the next state
45
- // change will trigger another render attempt.
46
- return false;
47
- }
48
- }
49
- async function listCloseouts(workspaceDir) {
50
- const dir = join(workspaceDir, "closeouts");
51
- if (!(await pathExists(dir)))
52
- return [];
53
- let entries;
54
- try {
55
- entries = await readdir(dir);
56
- }
57
- catch {
58
- return [];
59
- }
60
- const out = [];
61
- for (const name of entries) {
62
- const m = CLOSEOUT_FILENAME_RE.exec(name);
63
- if (!m)
64
- continue;
65
- out.push({ date: m[1], phaseOrModule: m[2], fileName: name, summary: await readCloseoutSummary(join(dir, name)) });
66
- }
67
- return out;
68
- }
69
- async function readCloseoutSummary(path) {
70
- try {
71
- const raw = await readFile(path, "utf8");
72
- const lines = raw.split(/\r?\n/);
73
- const summaryStart = lines.findIndex((line) => /^##\s+Summary\s*$/i.test(line.trim()));
74
- if (summaryStart === -1)
75
- return undefined;
76
- const body = [];
77
- for (const line of lines.slice(summaryStart + 1)) {
78
- if (/^##\s+/.test(line.trim()))
79
- break;
80
- const trimmed = line.trim();
81
- if (!trimmed || trimmed === "-")
82
- continue;
83
- body.push(trimmed.replace(/^[-*]\s+/, ""));
84
- if (body.join(" ").length > 280)
85
- break;
86
- }
87
- const summary = body.join(" ").trim();
88
- return summary ? `${summary.slice(0, 280)}${summary.length > 280 ? "…" : ""}` : undefined;
89
- }
90
- catch {
91
- return undefined;
92
- }
93
- }
94
- async function buildOutputsPresent(workspaceDir, pipeline) {
95
- const out = new Map();
96
- for (const phaseId of pipeline.phase_order) {
97
- const phaseDef = pipeline.phases.find((p) => p.id === phaseId);
98
- if (!phaseDef)
99
- continue;
100
- const entry = { secondary: [] };
101
- if (phaseDef.primary_output) {
102
- entry.primary = {
103
- path: phaseDef.primary_output,
104
- exists: await pathExists(join(workspaceDir, phaseDef.primary_output)),
105
- };
106
- }
107
- for (const sec of phaseDef.secondary_outputs ?? []) {
108
- entry.secondary.push({
109
- path: sec.path,
110
- exists: await pathExists(join(workspaceDir, sec.path)),
111
- });
112
- }
113
- out.set(phaseId, entry);
114
- }
115
- return out;
116
- }
117
- async function loadNarration(workspaceDir) {
118
- const path = join(workspaceDir, NARRATION_CACHE_RELATIVE_PATH);
119
- if (!(await pathExists(path)))
120
- return undefined;
121
- try {
122
- const raw = await readFile(path, "utf8");
123
- const { frontmatter, body } = splitFrontmatter(raw);
124
- if (!frontmatter)
125
- return undefined;
126
- const generatedAt = typeof frontmatter.generatedAt === "string" ? frontmatter.generatedAt : "";
127
- const phaseCountAtGeneration = typeof frontmatter.phaseCountAtGeneration === "number" ? frontmatter.phaseCountAtGeneration : 0;
128
- if (!generatedAt)
129
- return undefined;
130
- return { content: body.trim(), generatedAt, phaseCountAtGeneration };
131
- }
132
- catch {
133
- return undefined;
134
- }
135
- }
136
- function splitFrontmatter(raw) {
137
- if (!raw.startsWith("---\n"))
138
- return { frontmatter: null, body: raw };
139
- const end = raw.indexOf("\n---\n", 4);
140
- if (end === -1)
141
- return { frontmatter: null, body: raw };
142
- const yamlText = raw.slice(4, end);
143
- const body = raw.slice(end + 5);
144
- try {
145
- const parsed = parseSimpleYaml(yamlText);
146
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
147
- return { frontmatter: parsed, body };
148
- }
149
- }
150
- catch {
151
- // fall through
152
- }
153
- return { frontmatter: null, body };
154
- }
1
+ // Moved to core/dashboard-writer.ts (#254): both surfaces render the dashboard
2
+ // at the same lifecycle points, so it is a shared primitive. This re-export
3
+ // keeps the old import path — scripts/build-demo-dashboard.mjs among them —
4
+ // working.
5
+ export { writeDashboard } from "../../core/dashboard-writer.js";