codecartographer-pi 0.19.6 → 0.20.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 (39) hide show
  1. package/.codecarto/GUIDE.md +1 -1
  2. package/.codecarto/broadside/SKILL.md +14 -0
  3. package/.codecarto/broadside/config.yaml +18 -0
  4. package/.codecarto/workflow/VALIDATE.md +2 -1
  5. package/.codecarto/workflow/scaffold-version.yaml +1 -1
  6. package/README.md +10 -6
  7. package/dist/core/broadside.d.ts +72 -2
  8. package/dist/core/broadside.js +348 -63
  9. package/dist/core/completion.js +81 -22
  10. package/dist/core/dashboard-writer.d.ts +8 -0
  11. package/dist/core/dashboard-writer.js +159 -0
  12. package/dist/core/index.d.ts +2 -0
  13. package/dist/core/index.js +2 -0
  14. package/dist/core/library.js +4 -1
  15. package/dist/core/orchestrator-config.d.ts +32 -7
  16. package/dist/core/orchestrator-config.js +124 -44
  17. package/dist/core/pipeline.d.ts +37 -0
  18. package/dist/core/pipeline.js +80 -10
  19. package/dist/core/prompts.d.ts +20 -0
  20. package/dist/core/prompts.js +43 -10
  21. package/dist/core/secrets.d.ts +16 -0
  22. package/dist/core/secrets.js +98 -0
  23. package/dist/core/status.d.ts +8 -0
  24. package/dist/core/status.js +8 -3
  25. package/dist/core/synthesis.js +5 -2
  26. package/dist/core/workspace.d.ts +55 -8
  27. package/dist/core/workspace.js +115 -7
  28. package/dist/core/yaml.js +173 -14
  29. package/dist/extensions/codecarto/agent-rewriter.js +21 -14
  30. package/dist/extensions/codecarto/agent-runner.d.ts +6 -2
  31. package/dist/extensions/codecarto/agent-runner.js +27 -9
  32. package/dist/extensions/codecarto/agent-state.d.ts +0 -2
  33. package/dist/extensions/codecarto/auto-runner.js +10 -6
  34. package/dist/extensions/codecarto/dashboard-narrator.js +9 -2
  35. package/dist/extensions/codecarto/dashboard-writer.d.ts +1 -8
  36. package/dist/extensions/codecarto/dashboard-writer.js +5 -154
  37. package/dist/extensions/codecarto/index.js +65 -18
  38. package/dist/mcp-server/server.js +107 -49
  39. package/package.json +3 -2
@@ -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";
@@ -5,13 +5,12 @@ import { autoCompletePhase, buildAutoSummary, isPhaseRunning, runAuto, runSingle
5
5
  import { disposeAgentsWidget } from "./agent-widget.js";
6
6
  import { parseDashboardFlags } from "./dashboard-flags.js";
7
7
  import { narrateDashboard } from "./dashboard-narrator.js";
8
- import { writeDashboard } from "./dashboard-writer.js";
9
8
  import { parseBroadsideFlags, KNOWN_BROADSIDE_TOKENS } from "./broadside-flags.js";
10
9
  import { parseNextFlags } from "./next-flags.js";
11
10
  import { buildPiGuideMessage } from "./guide-framing.js";
12
11
  import { isCtxLive, notifyCtx } from "./notify.js";
13
12
  import { phaseCompactionExtension } from "./phase-compaction.js";
14
- import { applyAmendment, buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, copyPackagedWorkspace, computePerPhaseTotals, computeTotals, ConfidentialityMismatchError, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPath, resolveExistingPrefix, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, BroadsideCancelledError, broadsideDirFor, collectResultText, estimateSubmitText, getLens, listAmendmentNames, listBatchModels, listGuideTopics, listScaffoldRefreshFiles, listSkillNames, loadAmendmentFile, loadBroadsideConfig, modelsText, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, statusText, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, readBroadsideSkill, readGuide, refreshScaffold, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, resolvePublishSourceRepo, SourceRepoMismatchError, runPhasePreflight, SCAFFOLD_REFRESH_PROTECTED, seedOrchestratorFiles, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../../core/index.js";
13
+ import { applyAmendment, buildPhasePrompt, buildSkillPrompt, buildValidationSummary, backupWorkspaceState, canonicalPath, copyPackagedWorkspace, computePerPhaseTotals, computeTotals, ConfidentialityMismatchError, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, describeDanglingCarryForward, describeMissingCompletedOutputs, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPath, resolveExistingPrefix, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, BroadsideCancelledError, broadsideDirFor, collectResultText, estimateSubmitText, getLens, listAmendmentNames, listBatchModels, listGuideTopics, listScaffoldRefreshFiles, listMissingCompletedOutputs, listSkillNames, resolveSkillName, loadAmendmentFile, loadBroadsideConfig, modelsText, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, statusText, describeConfigProblems, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, readBroadsideSkill, readGuide, refreshScaffold, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, resolvePublishSourceRepo, SourceRepoMismatchError, runPhasePreflight, SCAFFOLD_REFRESH_PROTECTED, seedOrchestratorFiles, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, writeDashboard, } from "../../core/index.js";
15
14
  import { initLibrary } from "../../core/library.js";
16
15
  import { resolveUserConfigPath } from "../../core/orchestrator-config.js";
17
16
  const STATUS_WIDGET_ID = "codecarto-widget";
@@ -437,11 +436,21 @@ export default function codeCartographerExtension(pi) {
437
436
  const targetExists = await pathExists(targetWorkspaceDir);
438
437
  if (targetExists) {
439
438
  const sameWorkspace = normalizeForComparison(await canonicalPath(targetWorkspaceDir)) === normalizeForComparison(await canonicalPath(sourceWorkspaceDir));
440
- if (!sameWorkspace) {
441
- const overwrite = await ctx.ui.confirm("CodeCartographer already exists — data will be lost", "A .codecarto/ directory already exists in this repository. Re-initializing will back up the existing workspace to .codecarto-backup-TIMESTAMP/ and create a fresh one. All phase findings, handoffs, usage data, closeouts, and progress will be moved to the backup. Consider /codecarto-open to reattach without resetting. Continue?");
442
- if (!overwrite)
443
- return;
444
- const backupDir = join(ctx.cwd, `.codecarto-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`);
439
+ // The packaged template itself (a checkout install) is an existing
440
+ // workspace like any other: ask, and back up. It cannot be renamed
441
+ // away — it is what init copies from — so its session state is
442
+ // moved out file by file instead (#245).
443
+ const overwrite = await ctx.ui.confirm("CodeCartographer already exists — data will be lost", sameWorkspace
444
+ ? "This .codecarto/ is CodeCartographer's own packaged template (a checkout install), and it holds workspace state. Re-initializing will move that state — status, findings, handoffs, usage data, closeouts, dashboard — to .codecarto-backup-TIMESTAMP/ and reset; the framework files stay in place. Consider /codecarto-open to reattach without resetting. Continue?"
445
+ : "A .codecarto/ directory already exists in this repository. Re-initializing will back up the existing workspace to .codecarto-backup-TIMESTAMP/ and create a fresh one. All phase findings, handoffs, usage data, closeouts, and progress will be moved to the backup. Consider /codecarto-open to reattach without resetting. Continue?");
446
+ if (!overwrite)
447
+ return;
448
+ const backupDir = join(ctx.cwd, `.codecarto-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`);
449
+ if (sameWorkspace) {
450
+ const moved = await backupWorkspaceState(targetWorkspaceDir, backupDir);
451
+ notifyCtx(ctx, `Moved ${moved.length} workspace state file${moved.length === 1 ? "" : "s"} to ${basename(backupDir)}/`, "info");
452
+ }
453
+ else {
445
454
  await rename(targetWorkspaceDir, backupDir);
446
455
  notifyCtx(ctx, `Backed up existing workspace to ${basename(backupDir)}/`, "info");
447
456
  }
@@ -490,9 +499,17 @@ export default function codeCartographerExtension(pi) {
490
499
  if (!state)
491
500
  return;
492
501
  const nextPhase = getNextEligiblePhase(state)?.id ?? "complete";
493
- lastFeedbackLines = [`Current phase: ${nextPhase}`, `Pipeline: ${getPipelineLabel(state.status.pipeline)}`];
502
+ // Same check codecarto_status makes: a phase complete in status.yaml
503
+ // whose report is not on disk (#259).
504
+ const missingOutputs = describeMissingCompletedOutputs(await listMissingCompletedOutputs(state));
505
+ lastFeedbackLines = [`Current phase: ${nextPhase}`, `Pipeline: ${getPipelineLabel(state.status.pipeline)}`, ...missingOutputs];
494
506
  setUiState(ctx, state, lastFeedbackLines);
495
- notifyCtx(ctx, `CodeCartographer phase: ${nextPhase}`, "info");
507
+ if (missingOutputs.length > 0) {
508
+ notifyCtx(ctx, `CodeCartographer phase: ${nextPhase}. ${missingOutputs[0]}`, "warning");
509
+ }
510
+ else {
511
+ notifyCtx(ctx, `CodeCartographer phase: ${nextPhase}`, "info");
512
+ }
496
513
  },
497
514
  });
498
515
  pi.registerCommand("codecarto-switch-pipeline", {
@@ -533,6 +550,7 @@ export default function codeCartographerExtension(pi) {
533
550
  lines.push(`New phases: ${result.newPhases.join(", ")}`);
534
551
  if (result.dropped.length > 0)
535
552
  lines.push(`Phases not in new pipeline: ${result.dropped.join(", ")} (findings remain on disk)`);
553
+ lines.push(...describeDanglingCarryForward(result.dangling));
536
554
  lastFeedbackLines = lines;
537
555
  await refreshWorkspaceUi(ctx, lastFeedbackLines);
538
556
  notifyCtx(ctx, `Switched to pipeline: ${getPipelineLabel(pipelineChoice)}`, "info");
@@ -631,6 +649,10 @@ export default function codeCartographerExtension(pi) {
631
649
  return;
632
650
  }
633
651
  const config = await loadCodecartoConfig(state.workspaceDir);
652
+ // A bad config file must not block a phase run, but it must not be
653
+ // silent either: the toggle read here may be the one it dropped.
654
+ if (config.problems.length > 0)
655
+ notifyCtx(ctx, describeConfigProblems(config).join("\n"), "warning");
634
656
  const llmSteerEnabled = flags.llmSteerOverride ?? config.orchestrator.llm_steer_next_phase;
635
657
  lastFeedbackLines = [`Running ${phase.id} phase as sub-agent`];
636
658
  setUiState(ctx, state, lastFeedbackLines);
@@ -851,23 +873,27 @@ export default function codeCartographerExtension(pi) {
851
873
  notifyCtx(ctx, `Cannot run skill: pipeline is not complete (next phase: ${nextPhase.id}). Finish the pipeline before running post-pipeline skills.`, "error");
852
874
  return;
853
875
  }
854
- const skillFile = join(state.workspaceDir, "skills", skillName, "SKILL.md");
855
- if (!(await pathExists(skillFile))) {
876
+ // Resolve against the installed list only: the name is never joined
877
+ // onto a path, so a traversal like `../findings/architecture` cannot
878
+ // splice a phase SKILL.md into the post-pipeline prompt. Same rule as
879
+ // the MCP surface's handleSkill.
880
+ const resolved = await resolveSkillName(state.workspaceDir, skillName);
881
+ if (!resolved) {
856
882
  const available = await listSkillNames(state.workspaceDir);
857
883
  const hint = available.length > 0 ? ` (available: ${available.join(", ")})` : " (no skills installed)";
858
884
  notifyCtx(ctx, `Unknown skill: ${skillName}${hint}. The Broad-Side reading guide is served as \`${BROADSIDE_SKILL_NAME}\` and is not pipeline-gated.`, "error");
859
885
  return;
860
886
  }
861
- const prompt = await buildSkillPrompt(state, skillName);
887
+ const prompt = await buildSkillPrompt(state, resolved);
862
888
  if (ctx.isIdle()) {
863
889
  pi.sendUserMessage(prompt);
864
890
  }
865
891
  else {
866
892
  pi.sendUserMessage(prompt, { deliverAs: "followUp" });
867
893
  }
868
- lastFeedbackLines = [`Queued post-pipeline skill: ${skillName}`];
894
+ lastFeedbackLines = [`Queued post-pipeline skill: ${resolved}`];
869
895
  setUiState(ctx, state, lastFeedbackLines);
870
- notifyCtx(ctx, `Queued CodeCartographer skill: ${skillName}`, "info");
896
+ notifyCtx(ctx, `Queued CodeCartographer skill: ${resolved}`, "info");
871
897
  },
872
898
  });
873
899
  pi.registerCommand("codecarto-list-skills", {
@@ -1108,6 +1134,13 @@ export default function codeCartographerExtension(pi) {
1108
1134
  if (!state)
1109
1135
  return;
1110
1136
  const config = await loadCodecartoConfig(state.workspaceDir);
1137
+ // The library path, namespace, and confirm gate all come from the
1138
+ // config, so a file that could not be used is a refusal, not a silent
1139
+ // fallback to the other layer (#242). Same rule as codecarto_publish.
1140
+ if (config.problems.length > 0) {
1141
+ notifyCtx(ctx, ["Publish refused: the configuration has problems. Fix or remove the offending file, then retry.", ...describeConfigProblems(config)].join("\n"), "error");
1142
+ return;
1143
+ }
1111
1144
  if (!config.library.path) {
1112
1145
  notifyCtx(ctx, "No library.path is configured. Create a library directory with a .codecarto-library marker, then set library.path in ~/.codecarto/config.yaml or .codecarto/workflow/config.yaml.", "error");
1113
1146
  return;
@@ -1228,15 +1261,21 @@ export default function codeCartographerExtension(pi) {
1228
1261
  namespaced: !!namespace,
1229
1262
  ...(namespace ? {} : {}),
1230
1263
  });
1231
- // Write the config to the user-global location
1264
+ // Write library.path (and library.namespace when given) to the
1265
+ // user-global config and nothing else — not publish_confirm, so
1266
+ // initializing a library does not change how publish behaves on
1267
+ // MCP (#244). A config file that cannot be parsed is left alone;
1268
+ // the error lands in the catch below.
1232
1269
  const configPath = resolveUserConfigPath();
1233
1270
  await writeLibraryConfig(configPath, libraryPath, namespace);
1271
+ const written = namespace ? "library.path and library.namespace" : "library.path";
1234
1272
  const msg = result.alreadyExisted
1235
1273
  ? `Library already exists at ${libraryPath} (marker preserved). Config updated.`
1236
1274
  : `Created library at ${libraryPath} with marker "${result.marker.name}".`;
1237
- lastFeedbackLines = [msg, `Config written to ${configPath}`];
1275
+ const configLine = `Wrote ${written} to ${configPath}; other keys untouched`;
1276
+ lastFeedbackLines = [msg, configLine];
1238
1277
  notifyCtx(ctx, msg, "info");
1239
- notifyCtx(ctx, `Config written to ${configPath}`, "info");
1278
+ notifyCtx(ctx, configLine, "info");
1240
1279
  }
1241
1280
  catch (error) {
1242
1281
  const message = error instanceof Error ? error.message : String(error);
@@ -1263,10 +1302,18 @@ export default function codeCartographerExtension(pi) {
1263
1302
  const marker = await discoverLibrary(config.library.path);
1264
1303
  lines.push(` Library marker: ${marker ? `found ("${marker.name}", namespaced: ${marker.namespaced})` : "MISSING — run /codecarto-library-init"}`);
1265
1304
  }
1305
+ // A file that could not be used is the one thing this command exists
1306
+ // to surface; /codecarto-publish refuses while any is listed (#242).
1307
+ lines.push(...describeConfigProblems(config));
1266
1308
  lastFeedbackLines = lines;
1267
1309
  if (state)
1268
1310
  setUiState(ctx, state, lastFeedbackLines);
1269
- notifyCtx(ctx, "Configuration shown in status widget.", "info");
1311
+ if (config.problems.length > 0) {
1312
+ notifyCtx(ctx, describeConfigProblems(config).join("\n"), "warning");
1313
+ }
1314
+ else {
1315
+ notifyCtx(ctx, "Configuration shown in status widget.", "info");
1316
+ }
1270
1317
  },
1271
1318
  });
1272
1319
  pi.registerCommand("codecarto-usage", {