codecartographer-pi 0.16.0 → 0.17.1

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 (77) hide show
  1. package/.codecarto/GUIDE.md +16 -3
  2. package/.codecarto/README.md +3 -0
  3. package/.codecarto/broadside/SKILL.md +143 -0
  4. package/.codecarto/broadside/config.yaml +104 -0
  5. package/.codecarto/findings/architecture/SKILL.md +1 -0
  6. package/.codecarto/findings/broadside-scout/README.md +20 -0
  7. package/.codecarto/findings/broadside-scout/SKILL.md +101 -0
  8. package/.codecarto/findings/contracts/SKILL.md +1 -0
  9. package/.codecarto/findings/defect-scan/SKILL.md +15 -1
  10. package/.codecarto/findings/defect-scan/passes/01-logic-and-correctness.md +1 -1
  11. package/.codecarto/findings/defect-scan/passes/02-error-handling.md +1 -1
  12. package/.codecarto/findings/defect-scan/passes/03-concurrency-and-resources.md +1 -1
  13. package/.codecarto/findings/defect-scan/passes/04-security-and-trust.md +1 -1
  14. package/.codecarto/findings/defect-scan/passes/05-api-contract-violations.md +2 -1
  15. package/.codecarto/findings/defect-scan/passes/06-config-and-environment.md +1 -1
  16. package/.codecarto/findings/defect-scan-mechanical/SKILL.md +3 -2
  17. package/.codecarto/findings/defect-scan-semantic/SKILL.md +2 -0
  18. package/.codecarto/findings/porting/SKILL.md +2 -1
  19. package/.codecarto/findings/protocols/SKILL.md +1 -0
  20. package/.codecarto/findings/reimplementation-spec/SKILL.md +1 -1
  21. package/.codecarto/skills/spec-delta-application/SKILL.md +3 -1
  22. package/.codecarto/templates/architecture-map.md +1 -1
  23. package/.codecarto/templates/backlog-project.md +51 -0
  24. package/.codecarto/templates/broadside-scout-brief.md +97 -0
  25. package/.codecarto/templates/defect-report.md +23 -0
  26. package/.codecarto/templates/mechanical-defects.md +22 -0
  27. package/.codecarto/templates/reverse-engineering-bundle.md +2 -2
  28. package/.codecarto/templates/semantic-defects.md +26 -0
  29. package/.codecarto/{THREAD_LOG.md → templates/thread-log.md} +2 -5
  30. package/.codecarto/workflow/VALIDATE.md +1 -1
  31. package/.codecarto/workflow/pipeline-defect-scan.yaml +2 -0
  32. package/.codecarto/workflow/pipeline-full-with-audit.yaml +3 -1
  33. package/.codecarto/workflow/pipeline-full-with-deep-audit.yaml +5 -1
  34. package/.codecarto/workflow/pipeline-scout-first.yaml +275 -0
  35. package/.codecarto/workflow/scaffold-version.yaml +1 -1
  36. package/README.md +51 -6
  37. package/agent-skill/codecartographer/SKILL.md +3 -1
  38. package/agent-skill/codecartographer/references/broadside.md +115 -0
  39. package/agent-skill/codecartographer/references/deep-audit-synthesis.md +4 -1
  40. package/agent-skill/codecartographer/references/library.md +2 -2
  41. package/agent-skill/codecartographer/references/orchestration.md +1 -1
  42. package/agent-skill/codecartographer/references/pipeline-selection.md +14 -0
  43. package/dist/core/amendment.js +2 -2
  44. package/dist/core/broadside.d.ts +421 -0
  45. package/dist/core/broadside.js +2349 -0
  46. package/dist/core/completion.d.ts +5 -0
  47. package/dist/core/completion.js +38 -6
  48. package/dist/core/dashboard.js +5 -3
  49. package/dist/core/findings.d.ts +59 -0
  50. package/dist/core/findings.js +145 -0
  51. package/dist/core/index.d.ts +2 -0
  52. package/dist/core/index.js +2 -0
  53. package/dist/core/library.d.ts +88 -1
  54. package/dist/core/library.js +260 -7
  55. package/dist/core/orchestrator-config.js +5 -2
  56. package/dist/core/pipeline.js +16 -0
  57. package/dist/core/prompts.js +1 -1
  58. package/dist/core/status.js +23 -7
  59. package/dist/core/types.d.ts +6 -0
  60. package/dist/core/utils.d.ts +14 -0
  61. package/dist/core/utils.js +37 -1
  62. package/dist/core/workspace.d.ts +17 -0
  63. package/dist/core/workspace.js +79 -20
  64. package/dist/core/yaml.js +19 -4
  65. package/dist/extensions/codecarto/agent-runner.js +6 -0
  66. package/dist/extensions/codecarto/auto-runner.d.ts +2 -0
  67. package/dist/extensions/codecarto/broadside-flags.d.ts +26 -0
  68. package/dist/extensions/codecarto/broadside-flags.js +129 -0
  69. package/dist/extensions/codecarto/dashboard-narrator.js +1 -1
  70. package/dist/extensions/codecarto/index.js +270 -18
  71. package/dist/extensions/codecarto/phase-compaction.js +4 -0
  72. package/dist/mcp-server/server.d.ts +22 -0
  73. package/dist/mcp-server/server.js +282 -17
  74. package/package.json +11 -2
  75. package/.codecarto/BACKLOG.md +0 -184
  76. package/.codecarto/CHANGELOG-2026-05-02-feedback-pass.md +0 -118
  77. package/.codecarto/closeouts/2026-05-02-framework-feedback-pass.md +0 -111
@@ -1,4 +1,4 @@
1
- import { cp, mkdir, readFile, rename, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import { basename, join, resolve } from "node:path";
4
4
  import { autoCompletePhase, buildAutoSummary, isPhaseRunning, runAuto, runSinglePhase } from "./auto-runner.js";
@@ -6,12 +6,16 @@ import { disposeAgentsWidget } from "./agent-widget.js";
6
6
  import { parseDashboardFlags } from "./dashboard-flags.js";
7
7
  import { narrateDashboard } from "./dashboard-narrator.js";
8
8
  import { writeDashboard } from "./dashboard-writer.js";
9
+ import { parseBroadsideFlags, KNOWN_BROADSIDE_TOKENS } from "./broadside-flags.js";
9
10
  import { parseNextFlags } from "./next-flags.js";
10
11
  import { phaseCompactionExtension } from "./phase-compaction.js";
11
- import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPathResolved, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, runPhasePreflight, seedOrchestratorFiles, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../../core/index.js";
12
+ import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, copyPackagedWorkspace, computePerPhaseTotals, computeTotals, ConfidentialityMismatchError, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPathResolved, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, BroadsideCancelledError, broadsideDirFor, collectResultText, estimateSubmitText, getLens, listBatchModels, listSkillNames, loadBroadsideConfig, modelsText, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, statusText, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, readBroadsideSkill, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, runPhasePreflight, seedOrchestratorFiles, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../../core/index.js";
12
13
  import { initLibrary } from "../../core/library.js";
13
14
  import { resolveUserConfigPath } from "../../core/orchestrator-config.js";
14
15
  const STATUS_WIDGET_ID = "codecarto-widget";
16
+ // Broad-Side gets its own widget id: a scout run is legal on a repository with
17
+ // no workspace, where the phase widget has nothing to render.
18
+ const BROADSIDE_WIDGET_ID = "codecarto-broadside";
15
19
  const STATUS_LINE_ID = "codecarto-status";
16
20
  const SAFE_TOOL_NAMES = ["read", "grep", "find", "ls", "edit", "write"];
17
21
  function derivePublishHeadline(spec, cwd) {
@@ -92,6 +96,47 @@ function setUiState(ctx, state, extraLines = []) {
92
96
  ctx.ui.setStatus(STATUS_LINE_ID, `${theme.fg("accent", "CC")} ${theme.fg("dim", currentPhase)}`);
93
97
  ctx.ui.setWidget(STATUS_WIDGET_ID, buildStatusLines(state, extraLines));
94
98
  }
99
+ /**
100
+ * Resolve the OpenRouter key for a Broad-Side run. Deliberately no slash-command
101
+ * parameter: a key typed as a command argument lands in the session transcript.
102
+ */
103
+ function resolveBroadsideKey(configuredKey) {
104
+ const fromEnv = process.env.OPENROUTER_API_KEY?.trim();
105
+ if (fromEnv)
106
+ return fromEnv;
107
+ return configuredKey.trim() || null;
108
+ }
109
+ /** The spend decision, rendered for a human about to approve it. */
110
+ function describeBroadsideEstimate(estimate) {
111
+ const lines = [
112
+ `Model: ${estimate.model}${estimate.mixedModels ? " (some lenses overridden — see below)" : ""} (pricing: ${estimate.pricing.source})`,
113
+ `Rates: $${estimate.pricing.inputPerM.toFixed(4)}/M in · $${estimate.pricing.outputPerM.toFixed(4)}/M out`,
114
+ "",
115
+ "Per lens:",
116
+ ...estimate.lenses.map(({ name, slices, cost, model }) => {
117
+ // Naming the model only when it differs keeps the common case quiet
118
+ // and makes a mixed-model run impossible to approve without noticing.
119
+ const override = estimate.mixedModels && model !== estimate.model ? ` on ${model}` : "";
120
+ return ` ${name}: ${slices} slice${slices === 1 ? "" : "s"} — ~$${cost.toFixed(4)}${override}`;
121
+ }),
122
+ "",
123
+ `Estimated total: ~$${estimate.totalCost.toFixed(4)} ` +
124
+ `(~${Math.round(estimate.inputTokens / 1000)}k in, ~${Math.round(estimate.outputTokens / 1000)}k out)`,
125
+ ];
126
+ if (estimate.maxCost > 0) {
127
+ lines.push(estimate.exceedsLimit
128
+ ? `This EXCEEDS the configured max_cost of $${estimate.maxCost.toFixed(2)}. Approving here overrides it for this run.`
129
+ : `Within the configured max_cost of $${estimate.maxCost.toFixed(2)}.`);
130
+ }
131
+ if (estimate.baseHead) {
132
+ lines.push(`Incremental: only modules changed since ${estimate.baseHead.slice(0, 8)} are included.`);
133
+ }
134
+ else if (estimate.sourceDirty) {
135
+ lines.push("Incremental was requested but the tree is dirty — this is a full scan.");
136
+ }
137
+ lines.push("", "The estimate is a pre-flight prediction from file sizes; OpenRouter bills actual usage.");
138
+ return lines.join("\n");
139
+ }
95
140
  export default function codeCartographerExtension(pi) {
96
141
  phaseCompactionExtension(pi);
97
142
  let lastFeedbackLines = [];
@@ -268,7 +313,7 @@ export default function codeCartographerExtension(pi) {
268
313
  }
269
314
  if (!(await pathExists(targetWorkspaceDir))) {
270
315
  await mkdir(ctx.cwd, { recursive: true });
271
- await cp(sourceWorkspaceDir, targetWorkspaceDir, { recursive: true });
316
+ await copyPackagedWorkspace(targetWorkspaceDir);
272
317
  }
273
318
  const rawStatusPath = join(targetWorkspaceDir, "workflow", "status.yaml");
274
319
  const rawStatus = (await loadYamlFile(rawStatusPath)) ?? {};
@@ -573,7 +618,7 @@ export default function codeCartographerExtension(pi) {
573
618
  ctx.ui.notify(`Cannot complete ${validation.phaseId}: ${validation.overall}`, "error");
574
619
  return;
575
620
  }
576
- const { updatedState, closeoutNotice } = await autoCompletePhase(ctx, validation);
621
+ const { updatedState, closeoutNotice, warnings } = await autoCompletePhase(ctx, validation);
577
622
  lastFeedbackLines = [
578
623
  `Completed phase: ${validation.phaseId}`,
579
624
  `Validation: ${validation.overall}`,
@@ -581,10 +626,15 @@ export default function codeCartographerExtension(pi) {
581
626
  ];
582
627
  if (closeoutNotice)
583
628
  lastFeedbackLines.push(closeoutNotice);
629
+ const notes = [...(validation.warnings ?? []), ...warnings];
630
+ for (const note of notes)
631
+ lastFeedbackLines.push(`NOTE: ${note} Non-gating.`);
584
632
  setUiState(ctx, updatedState, lastFeedbackLines);
585
- ctx.ui.notify(`Marked ${validation.phaseId} complete`, validation.overall === "PASS WITH GAPS" ? "warning" : "info");
633
+ ctx.ui.notify(`Marked ${validation.phaseId} complete`, validation.overall === "PASS WITH GAPS" || notes.length > 0 ? "warning" : "info");
586
634
  if (closeoutNotice)
587
635
  ctx.ui.notify(closeoutNotice, "info");
636
+ for (const note of notes)
637
+ ctx.ui.notify(note, "warning");
588
638
  },
589
639
  });
590
640
  pi.registerCommand("codecarto-skill", {
@@ -597,6 +647,30 @@ export default function codeCartographerExtension(pi) {
597
647
  ctx.ui.notify(`Usage: /codecarto-skill <name>${hint}`, "warning");
598
648
  return;
599
649
  }
650
+ // Broad-Side is a reading guide for batch reconnaissance output, not a
651
+ // post-pipeline skill: it is read before or during the pipeline and works
652
+ // on a repository with scout state and no workspace. Same exemption the
653
+ // MCP surface makes in handleSkill.
654
+ if (skillName === BROADSIDE_SKILL_NAME) {
655
+ const skill = await readBroadsideSkill(ctx.cwd).catch(() => null);
656
+ if (!skill) {
657
+ ctx.ui.notify("Broad-Side reading guide not found. Reinstall codecartographer-pi.", "error");
658
+ return;
659
+ }
660
+ const message = [
661
+ "Read the Broad-Side reading guide below and apply it to the batch reconnaissance results in .codecarto/broadside/.",
662
+ "",
663
+ skill.content,
664
+ ].join("\n");
665
+ if (ctx.isIdle()) {
666
+ pi.sendUserMessage(message);
667
+ }
668
+ else {
669
+ pi.sendUserMessage(message, { deliverAs: "followUp" });
670
+ }
671
+ ctx.ui.notify("Queued the Broad-Side reading guide", "info");
672
+ return;
673
+ }
600
674
  const state = await ensureWorkspaceState(ctx);
601
675
  if (!state)
602
676
  return;
@@ -609,7 +683,7 @@ export default function codeCartographerExtension(pi) {
609
683
  if (!(await pathExists(skillFile))) {
610
684
  const available = await listSkillNames(state.workspaceDir);
611
685
  const hint = available.length > 0 ? ` (available: ${available.join(", ")})` : " (no skills installed)";
612
- ctx.ui.notify(`Unknown skill: ${skillName}${hint}`, "error");
686
+ ctx.ui.notify(`Unknown skill: ${skillName}${hint}. The Broad-Side reading guide is served as \`${BROADSIDE_SKILL_NAME}\` and is not pipeline-gated.`, "error");
613
687
  return;
614
688
  }
615
689
  const prompt = await buildSkillPrompt(state, skillName);
@@ -624,6 +698,167 @@ export default function codeCartographerExtension(pi) {
624
698
  ctx.ui.notify(`Queued CodeCartographer skill: ${skillName}`, "info");
625
699
  },
626
700
  });
701
+ pi.registerCommand("codecarto-broadside", {
702
+ description: "Batch reconnaissance (Broad-Side): /codecarto-broadside [submit|collect|status|models] [lenses…] [flags]",
703
+ getArgumentCompletions: (prefix) => {
704
+ const items = KNOWN_BROADSIDE_TOKENS
705
+ .filter((value) => value.startsWith(prefix))
706
+ .map((value) => ({ value, label: value }));
707
+ return items.length > 0 ? items : null;
708
+ },
709
+ handler: async (args, ctx) => {
710
+ const flags = parseBroadsideFlags(args);
711
+ if (flags.unknown.length > 0) {
712
+ ctx.ui.notify(`Unknown /codecarto-broadside argument: ${flags.unknown.join(" ")}. ` +
713
+ `Actions: submit, collect, status, models. Lenses: ${BROADSIDE_LENS_IDS.join(", ")}.`, "error");
714
+ return;
715
+ }
716
+ if (flags.error) {
717
+ ctx.ui.notify(flags.error, "error");
718
+ return;
719
+ }
720
+ // Broad-Side runs on any git repository, with or without a workspace —
721
+ // so this command never goes through ensureWorkspaceState.
722
+ const broadsideDir = broadsideDirFor(ctx.cwd);
723
+ const config = await loadBroadsideConfig(broadsideDir);
724
+ const finish = (lines, notice, level = "info") => {
725
+ lastFeedbackLines = lines;
726
+ if (codecartoModeActive) {
727
+ // A workspace session already has a widget; fold the result into it.
728
+ if (ctx.hasUI)
729
+ ctx.ui.setWidget(BROADSIDE_WIDGET_ID, undefined);
730
+ void refreshWorkspaceUi(ctx, lines);
731
+ }
732
+ else if (ctx.hasUI) {
733
+ // Scout-only repository: the Broad-Side widget is the only place
734
+ // the result can live, so it holds it instead of being cleared.
735
+ ctx.ui.setWidget(BROADSIDE_WIDGET_ID, ["Broad-Side", ...lines]);
736
+ }
737
+ ctx.ui.notify(notice, level);
738
+ };
739
+ if (flags.action === "status") {
740
+ const { state } = await runBroadsideStatus(ctx.cwd);
741
+ const runs = state.runs.length;
742
+ finish(statusText(state).split("\n"), runs > 0 ? `Broad-Side: ${runs} recorded run${runs === 1 ? "" : "s"}` : "Broad-Side: no runs recorded yet");
743
+ return;
744
+ }
745
+ const apiKey = resolveBroadsideKey(config.apiKey);
746
+ if (!apiKey) {
747
+ ctx.ui.notify("No OpenRouter API key. Set OPENROUTER_API_KEY in the environment, or add api_key to " +
748
+ ".codecarto/broadside/config.yaml. (A slash command takes no key: it would land in the transcript.)", "error");
749
+ return;
750
+ }
751
+ if (flags.action === "models") {
752
+ ctx.ui.notify("Fetching the OpenRouter batch-model catalog…", "info");
753
+ try {
754
+ const { entries, benchmarks } = await listBatchModels(broadsideDir, config, apiKey, {
755
+ includeBenchmarks: flags.benchmarks,
756
+ });
757
+ finish(modelsText(entries, { benchmarks, defaultModel: config.model }).split("\n"), `Broad-Side: ${entries.length} batch model${entries.length === 1 ? "" : "s"} listed`);
758
+ }
759
+ catch (error) {
760
+ ctx.ui.notify(`Model catalog lookup failed: ${error instanceof Error ? error.message : String(error)}`, "error");
761
+ }
762
+ return;
763
+ }
764
+ // Live per-lens progress. Poll callbacks fire often, so they render
765
+ // into a widget rather than a notification stream.
766
+ const progress = new Map();
767
+ const renderProgress = (heading) => {
768
+ if (!ctx.hasUI)
769
+ return;
770
+ ctx.ui.setWidget(BROADSIDE_WIDGET_ID, [
771
+ "Broad-Side",
772
+ heading,
773
+ ...[...progress.entries()].map(([lensId, line]) => ` ${lensId}: ${line}`),
774
+ ]);
775
+ };
776
+ const onStatus = (lensId, status, counts) => {
777
+ progress.set(lensId, `${status} (${counts.completed ?? 0}/${counts.total ?? "?"})`);
778
+ renderProgress("Polling batches…");
779
+ };
780
+ const waitSeconds = flags.waitSeconds ?? config.waitSeconds;
781
+ const waitMs = waitSeconds > 0 ? waitSeconds * 1000 : undefined;
782
+ const includeSynthesis = flags.includeSynthesis ?? config.includeSynthesis;
783
+ const includeTriage = flags.includeTriage ?? config.includeTriage;
784
+ const retryTruncated = flags.retryTruncated ?? config.retryTruncated;
785
+ if (flags.action === "submit") {
786
+ const lenses = flags.lenses.length > 0 ? flags.lenses : config.defaultLenses;
787
+ renderProgress("Slicing the repository and pricing the run…");
788
+ let submit;
789
+ try {
790
+ submit = await runBroadsideSubmit(ctx.cwd, apiKey, {
791
+ lenses,
792
+ model: config.model,
793
+ maxCost: flags.maxCost ?? config.maxCost,
794
+ // `??`, not `||`: --no-incremental parses to false and must beat a
795
+ // config-set true, exactly as MCP's `incremental: false` does (#163).
796
+ incremental: flags.incremental ?? config.incremental,
797
+ // Pi can ask, so it asks instead of refusing over max_cost the
798
+ // way MCP has to. An approval here IS the force flag.
799
+ confirm: (estimate) => ctx.ui.confirm(`Broad-Side will spend about $${estimate.totalCost.toFixed(4)}`, describeBroadsideEstimate(estimate)),
800
+ });
801
+ }
802
+ catch (error) {
803
+ if (ctx.hasUI)
804
+ ctx.ui.setWidget(BROADSIDE_WIDGET_ID, undefined);
805
+ if (error instanceof BroadsideCancelledError) {
806
+ ctx.ui.notify("Broad-Side cancelled. Nothing was submitted.", "info");
807
+ return;
808
+ }
809
+ ctx.ui.notify(`Broad-Side submit failed: ${error instanceof Error ? error.message : String(error)}`, "error");
810
+ return;
811
+ }
812
+ const lines = estimateSubmitText(submit, lenses.map(getLens)).split("\n");
813
+ if (!waitMs) {
814
+ lines.push("", "Batches are in flight. Run /codecarto-broadside collect when they finish.");
815
+ finish(lines, `Broad-Side submitted: run ${submit.runId} (~$${submit.estimatedTotalCost.toFixed(4)})`);
816
+ return;
817
+ }
818
+ ctx.ui.notify(`Broad-Side submitted run ${submit.runId}; polling for up to ${waitSeconds}s…`, "info");
819
+ try {
820
+ const collect = await runBroadsideCollect(ctx.cwd, apiKey, {
821
+ waitMs,
822
+ includeSynthesis,
823
+ includeTriage,
824
+ retryTruncated,
825
+ onStatus,
826
+ });
827
+ finish([...lines, "", ...collectResultText(collect).split("\n")], `Broad-Side ${collect.status}: run ${collect.runId}`);
828
+ }
829
+ catch (error) {
830
+ if (ctx.hasUI)
831
+ ctx.ui.setWidget(BROADSIDE_WIDGET_ID, undefined);
832
+ // The batches are submitted and paid for either way — say so, so
833
+ // nobody re-submits a run that is already in flight.
834
+ ctx.ui.notify(`Broad-Side submitted run ${submit.runId}, but collect failed: ` +
835
+ `${error instanceof Error ? error.message : String(error)}. Retry with /codecarto-broadside collect.`, "error");
836
+ }
837
+ return;
838
+ }
839
+ // action === "collect"
840
+ renderProgress("Polling batches…");
841
+ try {
842
+ const collect = await runBroadsideCollect(ctx.cwd, apiKey, {
843
+ waitMs,
844
+ includeSynthesis,
845
+ includeTriage,
846
+ retryTruncated,
847
+ onStatus,
848
+ });
849
+ const lines = collectResultText(collect).split("\n");
850
+ const done = collect.status === "completed";
851
+ if (!done)
852
+ lines.push("", "Still in flight. Run /codecarto-broadside collect again to resume.");
853
+ finish(lines, `Broad-Side ${collect.status}: ${collect.resultCount} result${collect.resultCount === 1 ? "" : "s"} saved`, done ? "info" : "warning");
854
+ }
855
+ catch (error) {
856
+ if (ctx.hasUI)
857
+ ctx.ui.setWidget(BROADSIDE_WIDGET_ID, undefined);
858
+ ctx.ui.notify(`Broad-Side collect failed: ${error instanceof Error ? error.message : String(error)}`, "error");
859
+ }
860
+ },
861
+ });
627
862
  pi.registerCommand("codecarto-publish", {
628
863
  description: "Publish the completed reimplementation spec to the configured CodeCartographer library",
629
864
  handler: async (_args, ctx) => {
@@ -667,19 +902,36 @@ export default function codeCartographerExtension(pi) {
667
902
  ].join("\n");
668
903
  if (config.library.publish_confirm && !(await ctx.ui.confirm("Publish reimplementation spec", preview)))
669
904
  return;
905
+ const input = {
906
+ slug,
907
+ namespace,
908
+ source_repo: ctx.cwd,
909
+ analyzed_at: new Date().toISOString(),
910
+ pipeline: state.status.pipeline,
911
+ codecarto_version: PACKAGE_VERSION,
912
+ headline,
913
+ tags: [],
914
+ capabilities: [],
915
+ generation: piGeneration(ctx),
916
+ };
670
917
  try {
671
- const result = await publishEntry(config.library.path, spec, {
672
- slug,
673
- namespace,
674
- source_repo: ctx.cwd,
675
- analyzed_at: new Date().toISOString(),
676
- pipeline: state.status.pipeline,
677
- codecarto_version: PACKAGE_VERSION,
678
- headline,
679
- tags: [],
680
- capabilities: [],
681
- generation: piGeneration(ctx),
682
- });
918
+ let result;
919
+ try {
920
+ result = await publishEntry(config.library.path, spec, input);
921
+ }
922
+ catch (error) {
923
+ if (!(error instanceof ConfidentialityMismatchError))
924
+ throw error;
925
+ // Nothing was written. Pi declares no confidentiality, so the entry
926
+ // sits at the internal default; whether it may go into a wider
927
+ // library is the user's call, and a yes is the override.
928
+ const publishAnyway = await ctx.ui.confirm("Confidentiality mismatch — publish anyway?", `This spec's confidentiality is "${error.entryConfidentiality}" (CodeCartographer's default; /codecarto-publish declares none), but the library "${marker.name}" has visibility "${error.libraryVisibility}". Publishing would expose it to everyone that library reaches. Publish anyway?`);
929
+ if (!publishAnyway) {
930
+ ctx.ui.notify("Publish cancelled. Nothing was written.", "info");
931
+ return;
932
+ }
933
+ result = await publishEntry(config.library.path, spec, input, { allowConfidentialityMismatch: true });
934
+ }
683
935
  lastFeedbackLines = [`Published ${result.namespace ? `${result.namespace}/` : ""}${result.slug} v${result.version}`, result.isNewVersion ? "New content version." : "Metadata-only update (content unchanged)."];
684
936
  await writeDashboard(ctx.cwd, PACKAGE_VERSION);
685
937
  await refreshWorkspaceUi(ctx, lastFeedbackLines);
@@ -81,6 +81,10 @@ export function phaseCompactionExtension(pi) {
81
81
  return undefined;
82
82
  try {
83
83
  const state = await getWorkspaceState(ctx.cwd);
84
+ // No workspace under this cwd: nothing phase-aware to add, so the host
85
+ // default applies — not a failure worth a warning (#134).
86
+ if (!state)
87
+ return undefined;
84
88
  const phase = state.pipeline.phases.find((candidate) => candidate.id === phaseId);
85
89
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
86
90
  if (!auth.ok || !auth.apiKey)
@@ -230,6 +230,28 @@ export declare function handleAmend(args: {
230
230
  text: string;
231
231
  };
232
232
  }>;
233
+ export declare function handleBroadside(args: {
234
+ cwd: string;
235
+ action: "submit" | "collect" | "status" | "models";
236
+ lenses?: string[];
237
+ api_key?: string;
238
+ wait_seconds?: number;
239
+ include_synthesis?: boolean;
240
+ include_triage?: boolean;
241
+ retry_truncated?: boolean;
242
+ max_cost?: number;
243
+ force?: boolean;
244
+ include_benchmarks?: boolean;
245
+ incremental?: boolean;
246
+ }): Promise<{
247
+ content: {
248
+ type: "text";
249
+ text: string;
250
+ }[];
251
+ structuredContent: {
252
+ text: string;
253
+ };
254
+ }>;
233
255
  export declare function handleGuide(args: {
234
256
  topic?: string;
235
257
  }): Promise<{