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
@@ -1,17 +1,15 @@
1
1
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
- import { homedir } from "node:os";
3
2
  import { basename, dirname, join, resolve } from "node:path";
4
3
  import { autoCompletePhase, buildAutoSummary, isPhaseRunning, runAuto, runSinglePhase } from "./auto-runner.js";
5
4
  import { disposeAgentsWidget } from "./agent-widget.js";
6
5
  import { parseDashboardFlags } from "./dashboard-flags.js";
7
6
  import { narrateDashboard } from "./dashboard-narrator.js";
8
- import { writeDashboard } from "./dashboard-writer.js";
9
7
  import { parseBroadsideFlags, KNOWN_BROADSIDE_TOKENS } from "./broadside-flags.js";
10
8
  import { parseNextFlags } from "./next-flags.js";
11
9
  import { buildPiGuideMessage } from "./guide-framing.js";
12
10
  import { isCtxLive, notifyCtx } from "./notify.js";
13
11
  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";
12
+ import { applyAmendment, buildPhasePrompt, buildSkillPrompt, buildValidationSummary, backupWorkspaceState, canonicalPath, copyPackagedWorkspace, computePerPhaseTotals, computeTotals, ConfidentialityMismatchError, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, describeDanglingCarryForward, describeMissingCompletedOutputs, describeStuckPipeline, expandTilde, getPipelineLabel, getWorkspaceState, isWithinPath, resolveExistingPrefix, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, BroadsideConfigError, defaultBroadsideConfig, BroadsideCancelledError, broadsideDirFor, collectResultText, estimateSubmitText, getLens, listAmendmentNames, listBatchModels, listGuideTopics, listScaffoldRefreshFiles, listMissingCompletedOutputs, listSkillNames, resolvePipelineOutcome, 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
13
  import { initLibrary } from "../../core/library.js";
16
14
  import { resolveUserConfigPath } from "../../core/orchestrator-config.js";
17
15
  const STATUS_WIDGET_ID = "codecarto-widget";
@@ -57,9 +55,23 @@ function formatUsageDuration(ms) {
57
55
  const seconds = Math.floor((ms % 60_000) / 1000);
58
56
  return `${minutes}m${seconds.toString().padStart(2, "0")}s`;
59
57
  }
58
+ /**
59
+ * The phase the status line and session name show: the eligible phase, the
60
+ * first blocked phase when the pipeline is stuck, or "complete" — never
61
+ * "complete" for a pipeline that cannot finish (#228).
62
+ */
63
+ function currentPhaseLabel(state) {
64
+ const outcome = resolvePipelineOutcome(state);
65
+ if (outcome.kind === "eligible")
66
+ return outcome.phase.id;
67
+ if (outcome.kind === "stuck")
68
+ return outcome.blocked[0]?.phaseId ?? "stuck";
69
+ return "complete";
70
+ }
60
71
  function buildStatusLines(state, extraLines = []) {
61
- const nextPhase = getNextEligiblePhase(state);
62
- const currentPhase = nextPhase?.id ?? state.status.current_phase ?? "complete";
72
+ const outcome = resolvePipelineOutcome(state);
73
+ const nextPhase = outcome.kind === "eligible" ? outcome.phase : null;
74
+ const currentPhase = currentPhaseLabel(state);
63
75
  const pipelineLabel = getPipelineLabel(state.status.pipeline);
64
76
  const completedCount = state.pipeline.phase_order.filter((phaseId) => state.status.phases[phaseId]?.status === "complete").length;
65
77
  const terminalOpenQuestions = Object.values(state.status.phases).reduce((sum, phase) => sum + (phase.open_questions?.length ?? 0), 0);
@@ -69,7 +81,7 @@ function buildStatusLines(state, extraLines = []) {
69
81
  const lines = [
70
82
  "CodeCartographer",
71
83
  `Phase: ${currentPhase}`,
72
- `Pipeline state: ${currentPhase === "complete" ? "complete" : "in progress"}`,
84
+ `Pipeline state: ${outcome.kind === "eligible" ? "in progress" : outcome.kind}`,
73
85
  `Pipeline: ${pipelineLabel}`,
74
86
  `Progress: ${completedCount}/${state.pipeline.phase_order.length} complete`,
75
87
  `Open questions (terminal unresolved): ${terminalOpenQuestions}`,
@@ -77,6 +89,8 @@ function buildStatusLines(state, extraLines = []) {
77
89
  `Post-pipeline work: ${postPipelinePending} pending`,
78
90
  `Next: ${nextAction}`,
79
91
  ];
92
+ if (outcome.kind === "stuck")
93
+ lines.push(describeStuckPipeline(outcome.blocked));
80
94
  const scaffoldNotice = describeScaffoldStaleness(state);
81
95
  if (scaffoldNotice)
82
96
  lines.push(`Scaffold: ${scaffoldNotice}`);
@@ -94,7 +108,7 @@ function setUiState(ctx, state, extraLines = []) {
94
108
  return;
95
109
  }
96
110
  const theme = ctx.ui.theme;
97
- const currentPhase = getNextEligiblePhase(state)?.id ?? state.status.current_phase ?? "complete";
111
+ const currentPhase = currentPhaseLabel(state);
98
112
  ctx.ui.setStatus(STATUS_LINE_ID, `${theme.fg("accent", "CC")} ${theme.fg("dim", currentPhase)}`);
99
113
  ctx.ui.setWidget(STATUS_WIDGET_ID, buildStatusLines(state, extraLines));
100
114
  }
@@ -292,7 +306,7 @@ export default function codeCartographerExtension(pi) {
292
306
  const state = await readWorkspaceState(ctx, false);
293
307
  setUiState(ctx, state, extraLines ?? lastFeedbackLines);
294
308
  if (state) {
295
- const phaseId = getNextEligiblePhase(state)?.id ?? state.status.current_phase;
309
+ const phaseId = currentPhaseLabel(state);
296
310
  if (phaseId)
297
311
  pi.setSessionName(`CodeCartographer: ${phaseId}`);
298
312
  }
@@ -437,11 +451,21 @@ export default function codeCartographerExtension(pi) {
437
451
  const targetExists = await pathExists(targetWorkspaceDir);
438
452
  if (targetExists) {
439
453
  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, "-")}`);
454
+ // The packaged template itself (a checkout install) is an existing
455
+ // workspace like any other: ask, and back up. It cannot be renamed
456
+ // away — it is what init copies from — so its session state is
457
+ // moved out file by file instead (#245).
458
+ const overwrite = await ctx.ui.confirm("CodeCartographer already exists — data will be lost", sameWorkspace
459
+ ? "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?"
460
+ : "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?");
461
+ if (!overwrite)
462
+ return;
463
+ const backupDir = join(ctx.cwd, `.codecarto-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`);
464
+ if (sameWorkspace) {
465
+ const moved = await backupWorkspaceState(targetWorkspaceDir, backupDir);
466
+ notifyCtx(ctx, `Moved ${moved.length} workspace state file${moved.length === 1 ? "" : "s"} to ${basename(backupDir)}/`, "info");
467
+ }
468
+ else {
445
469
  await rename(targetWorkspaceDir, backupDir);
446
470
  notifyCtx(ctx, `Backed up existing workspace to ${basename(backupDir)}/`, "info");
447
471
  }
@@ -489,10 +513,22 @@ export default function codeCartographerExtension(pi) {
489
513
  const state = await ensureWorkspaceState(ctx);
490
514
  if (!state)
491
515
  return;
492
- const nextPhase = getNextEligiblePhase(state)?.id ?? "complete";
493
- lastFeedbackLines = [`Current phase: ${nextPhase}`, `Pipeline: ${getPipelineLabel(state.status.pipeline)}`];
516
+ const outcome = resolvePipelineOutcome(state);
517
+ const nextPhase = currentPhaseLabel(state);
518
+ // Same check codecarto_status makes: a phase complete in status.yaml
519
+ // whose report is not on disk (#259).
520
+ const missingOutputs = describeMissingCompletedOutputs(await listMissingCompletedOutputs(state));
521
+ lastFeedbackLines = [`Current phase: ${nextPhase}`, `Pipeline: ${getPipelineLabel(state.status.pipeline)}`, ...missingOutputs];
494
522
  setUiState(ctx, state, lastFeedbackLines);
495
- notifyCtx(ctx, `CodeCartographer phase: ${nextPhase}`, "info");
523
+ if (outcome.kind === "stuck") {
524
+ notifyCtx(ctx, `CodeCartographer phase: ${nextPhase}. ${describeStuckPipeline(outcome.blocked)}`, "error");
525
+ }
526
+ else if (missingOutputs.length > 0) {
527
+ notifyCtx(ctx, `CodeCartographer phase: ${nextPhase}. ${missingOutputs[0]}`, "warning");
528
+ }
529
+ else {
530
+ notifyCtx(ctx, `CodeCartographer phase: ${nextPhase}`, "info");
531
+ }
496
532
  },
497
533
  });
498
534
  pi.registerCommand("codecarto-switch-pipeline", {
@@ -533,6 +569,7 @@ export default function codeCartographerExtension(pi) {
533
569
  lines.push(`New phases: ${result.newPhases.join(", ")}`);
534
570
  if (result.dropped.length > 0)
535
571
  lines.push(`Phases not in new pipeline: ${result.dropped.join(", ")} (findings remain on disk)`);
572
+ lines.push(...describeDanglingCarryForward(result.dangling));
536
573
  lastFeedbackLines = lines;
537
574
  await refreshWorkspaceUi(ctx, lastFeedbackLines);
538
575
  notifyCtx(ctx, `Switched to pipeline: ${getPipelineLabel(pipelineChoice)}`, "info");
@@ -590,7 +627,7 @@ export default function codeCartographerExtension(pi) {
590
627
  // the readout tracks progress live instead of staying frozen
591
628
  // at the initial phase until the whole auto run finishes.
592
629
  setUiState(ctx, advancedState, [`Auto pipeline${flags.strict ? " (strict)" : ""} running…`]);
593
- const phaseId = getNextEligiblePhase(advancedState)?.id ?? advancedState.status.current_phase;
630
+ const phaseId = currentPhaseLabel(advancedState);
594
631
  if (phaseId)
595
632
  pi.setSessionName(`CodeCartographer: ${phaseId}`);
596
633
  },
@@ -606,13 +643,21 @@ export default function codeCartographerExtension(pi) {
606
643
  notifyCtx(ctx, `Auto pipeline ${result.outcome}: ${result.phasesRun.length}/${result.totalPhases} phases.`, result.outcome === "complete" ? "info" : "warning");
607
644
  return;
608
645
  }
609
- const phase = getNextEligiblePhase(state);
610
- if (!phase) {
646
+ const outcome = resolvePipelineOutcome(state);
647
+ if (outcome.kind === "stuck") {
648
+ const message = describeStuckPipeline(outcome.blocked);
649
+ lastFeedbackLines = [message];
650
+ setUiState(ctx, state, lastFeedbackLines);
651
+ notifyCtx(ctx, message, "error");
652
+ return;
653
+ }
654
+ if (outcome.kind === "complete") {
611
655
  lastFeedbackLines = ["All phases complete."];
612
656
  setUiState(ctx, state, lastFeedbackLines);
613
657
  notifyCtx(ctx, "All CodeCartographer phases are complete.", "info");
614
658
  return;
615
659
  }
660
+ const phase = outcome.phase;
616
661
  let preflight;
617
662
  try {
618
663
  preflight = await runPhasePreflight(state, phase);
@@ -631,6 +676,10 @@ export default function codeCartographerExtension(pi) {
631
676
  return;
632
677
  }
633
678
  const config = await loadCodecartoConfig(state.workspaceDir);
679
+ // A bad config file must not block a phase run, but it must not be
680
+ // silent either: the toggle read here may be the one it dropped.
681
+ if (config.problems.length > 0)
682
+ notifyCtx(ctx, describeConfigProblems(config).join("\n"), "warning");
634
683
  const llmSteerEnabled = flags.llmSteerOverride ?? config.orchestrator.llm_steer_next_phase;
635
684
  lastFeedbackLines = [`Running ${phase.id} phase as sub-agent`];
636
685
  setUiState(ctx, state, lastFeedbackLines);
@@ -846,28 +895,36 @@ export default function codeCartographerExtension(pi) {
846
895
  const state = await ensureWorkspaceState(ctx);
847
896
  if (!state)
848
897
  return;
849
- const nextPhase = getNextEligiblePhase(state);
850
- if (nextPhase) {
851
- notifyCtx(ctx, `Cannot run skill: pipeline is not complete (next phase: ${nextPhase.id}). Finish the pipeline before running post-pipeline skills.`, "error");
898
+ const outcome = resolvePipelineOutcome(state);
899
+ if (outcome.kind === "eligible") {
900
+ notifyCtx(ctx, `Cannot run skill: pipeline is not complete (next phase: ${outcome.phase.id}). Finish the pipeline before running post-pipeline skills.`, "error");
852
901
  return;
853
902
  }
854
- const skillFile = join(state.workspaceDir, "skills", skillName, "SKILL.md");
855
- if (!(await pathExists(skillFile))) {
903
+ if (outcome.kind === "stuck") {
904
+ notifyCtx(ctx, `Cannot run skill: the pipeline is not complete. ${describeStuckPipeline(outcome.blocked)}`, "error");
905
+ return;
906
+ }
907
+ // Resolve against the installed list only: the name is never joined
908
+ // onto a path, so a traversal like `../findings/architecture` cannot
909
+ // splice a phase SKILL.md into the post-pipeline prompt. Same rule as
910
+ // the MCP surface's handleSkill.
911
+ const resolved = await resolveSkillName(state.workspaceDir, skillName);
912
+ if (!resolved) {
856
913
  const available = await listSkillNames(state.workspaceDir);
857
914
  const hint = available.length > 0 ? ` (available: ${available.join(", ")})` : " (no skills installed)";
858
915
  notifyCtx(ctx, `Unknown skill: ${skillName}${hint}. The Broad-Side reading guide is served as \`${BROADSIDE_SKILL_NAME}\` and is not pipeline-gated.`, "error");
859
916
  return;
860
917
  }
861
- const prompt = await buildSkillPrompt(state, skillName);
918
+ const prompt = await buildSkillPrompt(state, resolved);
862
919
  if (ctx.isIdle()) {
863
920
  pi.sendUserMessage(prompt);
864
921
  }
865
922
  else {
866
923
  pi.sendUserMessage(prompt, { deliverAs: "followUp" });
867
924
  }
868
- lastFeedbackLines = [`Queued post-pipeline skill: ${skillName}`];
925
+ lastFeedbackLines = [`Queued post-pipeline skill: ${resolved}`];
869
926
  setUiState(ctx, state, lastFeedbackLines);
870
- notifyCtx(ctx, `Queued CodeCartographer skill: ${skillName}`, "info");
927
+ notifyCtx(ctx, `Queued CodeCartographer skill: ${resolved}`, "info");
871
928
  },
872
929
  });
873
930
  pi.registerCommand("codecarto-list-skills", {
@@ -882,11 +939,13 @@ export default function codeCartographerExtension(pi) {
882
939
  const lines = skills.length > 0
883
940
  ? [`Available skills (${skills.length}):`, ...skills.map((name) => ` - ${name}`)]
884
941
  : ["No skills installed."];
885
- const nextPhase = getNextEligiblePhase(state);
942
+ const outcome = resolvePipelineOutcome(state);
886
943
  if (skills.length > 0) {
887
- lines.push(nextPhase
888
- ? `Post-pipeline skills unlock when the pipeline completes (next phase: ${nextPhase.id}).`
889
- : "Run one with /codecarto-skill <name>.");
944
+ lines.push(outcome.kind === "eligible"
945
+ ? `Post-pipeline skills unlock when the pipeline completes (next phase: ${outcome.phase.id}).`
946
+ : outcome.kind === "stuck"
947
+ ? `Post-pipeline skills unlock when the pipeline completes, and it cannot: ${describeStuckPipeline(outcome.blocked)}`
948
+ : "Run one with /codecarto-skill <name>.");
890
949
  }
891
950
  // Broad-Side is listed apart from the post-pipeline set because it
892
951
  // answers to /codecarto-skill without the completion gate.
@@ -962,7 +1021,23 @@ export default function codeCartographerExtension(pi) {
962
1021
  // Broad-Side runs on any git repository, with or without a workspace —
963
1022
  // so this command never goes through ensureWorkspaceState.
964
1023
  const broadsideDir = broadsideDirFor(ctx.cwd);
965
- const config = await loadBroadsideConfig(broadsideDir);
1024
+ // A config.yaml that exists but cannot be read refuses every action
1025
+ // that would act on it (#232); status only reads recorded runs, so it
1026
+ // answers and says the file is unreadable.
1027
+ let config;
1028
+ let configWarning = null;
1029
+ try {
1030
+ config = await loadBroadsideConfig(broadsideDir);
1031
+ }
1032
+ catch (error) {
1033
+ const message = error instanceof Error ? error.message : String(error);
1034
+ if (!(error instanceof BroadsideConfigError) || flags.action !== "status") {
1035
+ notifyCtx(ctx, `Broad-Side ${flags.action} refused: ${message}`, "error");
1036
+ return;
1037
+ }
1038
+ config = defaultBroadsideConfig();
1039
+ configWarning = message;
1040
+ }
966
1041
  const finish = (lines, notice, level = "info") => {
967
1042
  lastFeedbackLines = lines;
968
1043
  if (codecartoModeActive) {
@@ -979,9 +1054,18 @@ export default function codeCartographerExtension(pi) {
979
1054
  notifyCtx(ctx, notice, level);
980
1055
  };
981
1056
  if (flags.action === "status") {
982
- const { state } = await runBroadsideStatus(ctx.cwd);
1057
+ let state;
1058
+ try {
1059
+ ({ state } = await runBroadsideStatus(ctx.cwd));
1060
+ }
1061
+ catch (error) {
1062
+ // A corrupt state.json: nothing trustworthy to report, and the
1063
+ // error names the preserved copy (#233).
1064
+ notifyCtx(ctx, `Broad-Side status failed: ${error instanceof Error ? error.message : String(error)}`, "error");
1065
+ return;
1066
+ }
983
1067
  const runs = state.runs.length;
984
- finish(statusText(state).split("\n"), runs > 0 ? `Broad-Side: ${runs} recorded run${runs === 1 ? "" : "s"}` : "Broad-Side: no runs recorded yet");
1068
+ finish([...statusText(state).split("\n"), ...(configWarning ? [`Warning: ${configWarning}`] : [])], runs > 0 ? `Broad-Side: ${runs} recorded run${runs === 1 ? "" : "s"}` : "Broad-Side: no runs recorded yet", configWarning ? "warning" : "info");
985
1069
  return;
986
1070
  }
987
1071
  const apiKey = resolveBroadsideKey(config.apiKey);
@@ -1019,8 +1103,10 @@ export default function codeCartographerExtension(pi) {
1019
1103
  progress.set(lensId, `${status} (${counts.completed ?? 0}/${counts.total ?? "?"})`);
1020
1104
  renderProgress("Polling batches…");
1021
1105
  };
1106
+ // An explicit --wait=0 polls once and returns; it used to become
1107
+ // undefined and, in core, the 25-minute budget (#230).
1022
1108
  const waitSeconds = flags.waitSeconds ?? config.waitSeconds;
1023
- const waitMs = waitSeconds > 0 ? waitSeconds * 1000 : undefined;
1109
+ const waitMs = waitSeconds * 1000;
1024
1110
  const includeSynthesis = flags.includeSynthesis ?? config.includeSynthesis;
1025
1111
  const includeTriage = flags.includeTriage ?? config.includeTriage;
1026
1112
  const retryTruncated = flags.retryTruncated ?? config.retryTruncated;
@@ -1087,6 +1173,7 @@ export default function codeCartographerExtension(pi) {
1087
1173
  includeTriage,
1088
1174
  retryTruncated,
1089
1175
  onStatus,
1176
+ ...(flags.runId && { runId: flags.runId }),
1090
1177
  });
1091
1178
  const lines = collectResultText(collect).split("\n");
1092
1179
  const done = collect.status === "completed";
@@ -1108,6 +1195,13 @@ export default function codeCartographerExtension(pi) {
1108
1195
  if (!state)
1109
1196
  return;
1110
1197
  const config = await loadCodecartoConfig(state.workspaceDir);
1198
+ // The library path, namespace, and confirm gate all come from the
1199
+ // config, so a file that could not be used is a refusal, not a silent
1200
+ // fallback to the other layer (#242). Same rule as codecarto_publish.
1201
+ if (config.problems.length > 0) {
1202
+ notifyCtx(ctx, ["Publish refused: the configuration has problems. Fix or remove the offending file, then retry.", ...describeConfigProblems(config)].join("\n"), "error");
1203
+ return;
1204
+ }
1111
1205
  if (!config.library.path) {
1112
1206
  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
1207
  return;
@@ -1222,21 +1316,26 @@ export default function codeCartographerExtension(pi) {
1222
1316
  notifyCtx(ctx, "Usage: /codecarto-library-init <path> [--namespace <name>]", "warning");
1223
1317
  return;
1224
1318
  }
1225
- const libraryPath = pathArg.startsWith("~") ? join(homedir(), pathArg.slice(1)) : resolve(pathArg);
1319
+ // The same expansion the config loader applies, so `~user/x` and a
1320
+ // bare `~` mean the same thing everywhere (self-audit mech 6.14).
1321
+ const libraryPath = resolve(expandTilde(pathArg));
1226
1322
  try {
1227
- const result = await initLibrary(libraryPath, {
1228
- namespaced: !!namespace,
1229
- ...(namespace ? {} : {}),
1230
- });
1231
- // Write the config to the user-global location
1323
+ const result = await initLibrary(libraryPath, { namespaced: !!namespace });
1324
+ // Write library.path (and library.namespace when given) to the
1325
+ // user-global config and nothing else — not publish_confirm, so
1326
+ // initializing a library does not change how publish behaves on
1327
+ // MCP (#244). A config file that cannot be parsed is left alone;
1328
+ // the error lands in the catch below.
1232
1329
  const configPath = resolveUserConfigPath();
1233
1330
  await writeLibraryConfig(configPath, libraryPath, namespace);
1331
+ const written = namespace ? "library.path and library.namespace" : "library.path";
1234
1332
  const msg = result.alreadyExisted
1235
1333
  ? `Library already exists at ${libraryPath} (marker preserved). Config updated.`
1236
1334
  : `Created library at ${libraryPath} with marker "${result.marker.name}".`;
1237
- lastFeedbackLines = [msg, `Config written to ${configPath}`];
1335
+ const configLine = `Wrote ${written} to ${configPath}; other keys untouched`;
1336
+ lastFeedbackLines = [msg, configLine];
1238
1337
  notifyCtx(ctx, msg, "info");
1239
- notifyCtx(ctx, `Config written to ${configPath}`, "info");
1338
+ notifyCtx(ctx, configLine, "info");
1240
1339
  }
1241
1340
  catch (error) {
1242
1341
  const message = error instanceof Error ? error.message : String(error);
@@ -1263,10 +1362,18 @@ export default function codeCartographerExtension(pi) {
1263
1362
  const marker = await discoverLibrary(config.library.path);
1264
1363
  lines.push(` Library marker: ${marker ? `found ("${marker.name}", namespaced: ${marker.namespaced})` : "MISSING — run /codecarto-library-init"}`);
1265
1364
  }
1365
+ // A file that could not be used is the one thing this command exists
1366
+ // to surface; /codecarto-publish refuses while any is listed (#242).
1367
+ lines.push(...describeConfigProblems(config));
1266
1368
  lastFeedbackLines = lines;
1267
1369
  if (state)
1268
1370
  setUiState(ctx, state, lastFeedbackLines);
1269
- notifyCtx(ctx, "Configuration shown in status widget.", "info");
1371
+ if (config.problems.length > 0) {
1372
+ notifyCtx(ctx, describeConfigProblems(config).join("\n"), "warning");
1373
+ }
1374
+ else {
1375
+ notifyCtx(ctx, "Configuration shown in status widget.", "info");
1376
+ }
1270
1377
  },
1271
1378
  });
1272
1379
  pi.registerCommand("codecarto-usage", {
@@ -1412,9 +1519,13 @@ export default function codeCartographerExtension(pi) {
1412
1519
  notifyCtx(ctx, error instanceof Error ? error.message : String(error), "error");
1413
1520
  return;
1414
1521
  }
1415
- const nextPhase = getNextEligiblePhase(state);
1416
- if (nextPhase) {
1417
- notifyCtx(ctx, `Cannot amend: the pipeline is not complete (next phase: ${nextPhase.id}). `
1522
+ const outcome = resolvePipelineOutcome(state);
1523
+ if (outcome.kind === "stuck") {
1524
+ notifyCtx(ctx, `Cannot amend: the pipeline is not complete. ${describeStuckPipeline(outcome.blocked)}`, "error");
1525
+ return;
1526
+ }
1527
+ if (outcome.kind === "eligible") {
1528
+ notifyCtx(ctx, `Cannot amend: the pipeline is not complete (next phase: ${outcome.phase.id}). `
1418
1529
  + "Resolve open questions and routed items through that phase's handoff (open_question_closures / carry_forward_closures) instead.", "error");
1419
1530
  return;
1420
1531
  }
@@ -7,7 +7,11 @@ export function phaseIdFromSessionName(sessionName) {
7
7
  if (!sessionName?.startsWith(PHASE_SESSION_PREFIX))
8
8
  return null;
9
9
  const phaseId = sessionName.slice(PHASE_SESSION_PREFIX.length).trim();
10
- return /^[a-z0-9][a-z0-9-]*$/.test(phaseId) ? phaseId : null;
10
+ // The same alphabet assertSafePhaseId admits (core/status.ts). A narrower
11
+ // match here silently dropped the bash block, write confinement, and
12
+ // checkpointing for a custom pipeline whose ids used `.`, `_`, or a capital
13
+ // (self-audit mech 6.11).
14
+ return /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(phaseId) ? phaseId : null;
11
15
  }
12
16
  export function buildPhaseCompactionInstructions(phaseId, primaryOutput) {
13
17
  const output = primaryOutput ? `.codecarto/${primaryOutput}` : "the phase's declared primary output";
@@ -37,6 +37,7 @@ export declare function handleSwitchPipeline(args: {
37
37
  }>;
38
38
  export declare function handleNext(args: {
39
39
  cwd: string;
40
+ unattended?: boolean;
40
41
  }): Promise<{
41
42
  content: {
42
43
  type: "text";
@@ -49,6 +50,7 @@ export declare function handleNext(args: {
49
50
  export declare function handlePhase(args: {
50
51
  cwd: string;
51
52
  phase: string;
53
+ unattended?: boolean;
52
54
  }): Promise<{
53
55
  content: {
54
56
  type: "text";
@@ -130,7 +132,6 @@ export declare function handleLibraryInit(args: {
130
132
  library_path: string;
131
133
  name?: string;
132
134
  namespace?: string;
133
- cwd?: string;
134
135
  }): Promise<{
135
136
  content: {
136
137
  type: "text";
@@ -236,6 +237,7 @@ export declare function handleBroadside(args: {
236
237
  lenses?: string[];
237
238
  api_key?: string;
238
239
  wait_seconds?: number;
240
+ run_id?: string;
239
241
  include_synthesis?: boolean;
240
242
  include_triage?: boolean;
241
243
  retry_truncated?: boolean;