codecartographer-pi 0.15.0 → 0.17.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 (44) hide show
  1. package/.codecarto/GUIDE.md +15 -2
  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/broadside-scout/README.md +20 -0
  6. package/.codecarto/findings/broadside-scout/SKILL.md +101 -0
  7. package/.codecarto/skills/spec-delta-application/SKILL.md +3 -1
  8. package/.codecarto/templates/backlog-project.md +51 -0
  9. package/.codecarto/templates/broadside-scout-brief.md +97 -0
  10. package/.codecarto/{THREAD_LOG.md → templates/thread-log.md} +2 -5
  11. package/.codecarto/workflow/pipeline-scout-first.yaml +271 -0
  12. package/.codecarto/workflow/scaffold-version.yaml +1 -1
  13. package/README.md +47 -2
  14. package/agent-skill/codecartographer/SKILL.md +6 -1
  15. package/agent-skill/codecartographer/references/broadside.md +115 -0
  16. package/agent-skill/codecartographer/references/library.md +32 -0
  17. package/agent-skill/codecartographer/references/pipeline-selection.md +14 -0
  18. package/dist/core/amendment.js +4 -1
  19. package/dist/core/broadside.d.ts +421 -0
  20. package/dist/core/broadside.js +2349 -0
  21. package/dist/core/completion.js +49 -14
  22. package/dist/core/index.d.ts +1 -0
  23. package/dist/core/index.js +1 -0
  24. package/dist/core/library.d.ts +22 -0
  25. package/dist/core/library.js +101 -1
  26. package/dist/core/orchestrator-config.js +5 -2
  27. package/dist/core/pipeline.js +1 -0
  28. package/dist/core/status.d.ts +8 -0
  29. package/dist/core/status.js +31 -1
  30. package/dist/core/utils.js +7 -1
  31. package/dist/core/workspace.d.ts +17 -0
  32. package/dist/core/workspace.js +68 -2
  33. package/dist/extensions/codecarto/agent-runner.js +6 -0
  34. package/dist/extensions/codecarto/broadside-flags.d.ts +21 -0
  35. package/dist/extensions/codecarto/broadside-flags.js +116 -0
  36. package/dist/extensions/codecarto/dashboard-writer.d.ts +8 -1
  37. package/dist/extensions/codecarto/dashboard-writer.js +10 -1
  38. package/dist/extensions/codecarto/index.js +232 -4
  39. package/dist/mcp-server/server.d.ts +22 -0
  40. package/dist/mcp-server/server.js +241 -13
  41. package/package.json +10 -1
  42. package/.codecarto/BACKLOG.md +0 -184
  43. package/.codecarto/CHANGELOG-2026-05-02-feedback-pass.md +0 -118
  44. package/.codecarto/closeouts/2026-05-02-framework-feedback-pass.md +0 -111
@@ -0,0 +1,116 @@
1
+ // Argument parser for /codecarto-broadside. The grammar is one optional
2
+ // action followed by lens names and flags, in any order:
3
+ //
4
+ // /codecarto-broadside → submit, default lenses
5
+ // /codecarto-broadside submit architecture security → submit, two lenses
6
+ // /codecarto-broadside collect --wait=900
7
+ // /codecarto-broadside status
8
+ // /codecarto-broadside models --benchmarks
9
+ //
10
+ // Flags mirror the codecarto_broadside tool parameters, with the negative
11
+ // forms spelled out because a slash command has no place to pass `false`:
12
+ // --incremental --no-synthesis
13
+ // --max-cost=N --no-triage
14
+ // --wait=SECONDS --no-retry-truncated
15
+ // --benchmarks (models only)
16
+ //
17
+ // The parser never throws. index.ts decides how to surface unknown tokens and
18
+ // invalid combinations, matching parseNextFlags.
19
+ import { BROADSIDE_LENS_IDS } from "../../core/index.js";
20
+ const ACTIONS = new Set(["submit", "collect", "status", "models"]);
21
+ /** Every token the completer offers, in the order it offers them. */
22
+ export const KNOWN_BROADSIDE_TOKENS = [
23
+ "submit",
24
+ "collect",
25
+ "status",
26
+ "models",
27
+ ...BROADSIDE_LENS_IDS,
28
+ "--incremental",
29
+ "--max-cost=",
30
+ "--wait=",
31
+ "--no-synthesis",
32
+ "--no-triage",
33
+ "--no-retry-truncated",
34
+ "--benchmarks",
35
+ ];
36
+ // A numeric flag with a missing or unparseable value is an error, not a
37
+ // silent fallback to the config default: "--max-cost=" almost certainly means
38
+ // the user meant to cap the spend and mistyped it.
39
+ function parseNumeric(token, name, result) {
40
+ const raw = token.slice(name.length + 1);
41
+ const value = Number(raw);
42
+ if (!raw || !Number.isFinite(value) || value < 0) {
43
+ result.error = `${name} needs a non-negative number (got "${raw}").`;
44
+ return undefined;
45
+ }
46
+ return value;
47
+ }
48
+ export function parseBroadsideFlags(args) {
49
+ const tokens = args.trim().split(/\s+/).filter((token) => token.length > 0);
50
+ const result = {
51
+ action: "submit",
52
+ lenses: [],
53
+ incremental: false,
54
+ benchmarks: false,
55
+ unknown: [],
56
+ };
57
+ let actionSeen = false;
58
+ for (const token of tokens) {
59
+ if (!actionSeen && ACTIONS.has(token)) {
60
+ result.action = token;
61
+ actionSeen = true;
62
+ continue;
63
+ }
64
+ if (BROADSIDE_LENS_IDS.includes(token)) {
65
+ // A lens named twice is one lens, not two batches of it.
66
+ if (!result.lenses.includes(token))
67
+ result.lenses.push(token);
68
+ continue;
69
+ }
70
+ if (token === "--incremental") {
71
+ result.incremental = true;
72
+ continue;
73
+ }
74
+ if (token === "--no-synthesis") {
75
+ result.includeSynthesis = false;
76
+ continue;
77
+ }
78
+ if (token === "--no-triage") {
79
+ result.includeTriage = false;
80
+ continue;
81
+ }
82
+ if (token === "--no-retry-truncated") {
83
+ result.retryTruncated = false;
84
+ continue;
85
+ }
86
+ if (token === "--benchmarks") {
87
+ result.benchmarks = true;
88
+ continue;
89
+ }
90
+ if (token.startsWith("--max-cost=")) {
91
+ result.maxCost = parseNumeric(token, "--max-cost", result);
92
+ continue;
93
+ }
94
+ if (token.startsWith("--wait=")) {
95
+ result.waitSeconds = parseNumeric(token, "--wait", result);
96
+ continue;
97
+ }
98
+ result.unknown.push(token);
99
+ }
100
+ // Flags that only mean something for one action are refused rather than
101
+ // ignored: silently dropping --incremental on a collect would read as
102
+ // "collected incrementally", which is not a thing.
103
+ if (result.lenses.length > 0 && result.action !== "submit") {
104
+ result.error ??= `Lens names are only meaningful for submit (got action "${result.action}").`;
105
+ }
106
+ if (result.incremental && result.action !== "submit") {
107
+ result.error ??= `--incremental is only meaningful for submit (got action "${result.action}").`;
108
+ }
109
+ if (result.benchmarks && result.action !== "models") {
110
+ result.error ??= `--benchmarks is only meaningful for models (got action "${result.action}").`;
111
+ }
112
+ if (result.action === "status" && result.waitSeconds !== undefined) {
113
+ result.error ??= "--wait is only meaningful for submit and collect; status reads recorded state.";
114
+ }
115
+ return result;
116
+ }
@@ -1 +1,8 @@
1
- export declare function writeDashboard(cwd: string, packageVersion: string): Promise<void>;
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>;
@@ -6,11 +6,18 @@ import { readdir, readFile, rename, writeFile } from "node:fs/promises";
6
6
  import { join } from "node:path";
7
7
  import { DASHBOARD_RELATIVE_PATH, getWorkspaceState, loadUsage, NARRATION_CACHE_RELATIVE_PATH, parseSimpleYaml, pathExists, renderDashboard, } from "../../core/index.js";
8
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
+ */
9
16
  export async function writeDashboard(cwd, packageVersion) {
10
17
  try {
11
18
  const state = await getWorkspaceState(cwd);
12
19
  if (!state)
13
- return;
20
+ return false;
14
21
  const workspaceDir = state.workspaceDir;
15
22
  const [usage, closeouts, outputsPresent, narration] = await Promise.all([
16
23
  loadUsage(workspaceDir),
@@ -33,11 +40,13 @@ export async function writeDashboard(cwd, packageVersion) {
33
40
  const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
34
41
  await writeFile(tempPath, html, "utf8");
35
42
  await rename(tempPath, path);
43
+ return true;
36
44
  }
37
45
  catch {
38
46
  // Best-effort: a failed dashboard write must not surface as a phase
39
47
  // error. The user's pipeline state is unaffected; the next state
40
48
  // change will trigger another render attempt.
49
+ return false;
41
50
  }
42
51
  }
43
52
  async function listCloseouts(workspaceDir) {
@@ -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, 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)) ?? {};
@@ -597,6 +642,30 @@ export default function codeCartographerExtension(pi) {
597
642
  ctx.ui.notify(`Usage: /codecarto-skill <name>${hint}`, "warning");
598
643
  return;
599
644
  }
645
+ // Broad-Side is a reading guide for batch reconnaissance output, not a
646
+ // post-pipeline skill: it is read before or during the pipeline and works
647
+ // on a repository with scout state and no workspace. Same exemption the
648
+ // MCP surface makes in handleSkill.
649
+ if (skillName === BROADSIDE_SKILL_NAME) {
650
+ const skill = await readBroadsideSkill(ctx.cwd).catch(() => null);
651
+ if (!skill) {
652
+ ctx.ui.notify("Broad-Side reading guide not found. Reinstall codecartographer-pi.", "error");
653
+ return;
654
+ }
655
+ const message = [
656
+ "Read the Broad-Side reading guide below and apply it to the batch reconnaissance results in .codecarto/broadside/.",
657
+ "",
658
+ skill.content,
659
+ ].join("\n");
660
+ if (ctx.isIdle()) {
661
+ pi.sendUserMessage(message);
662
+ }
663
+ else {
664
+ pi.sendUserMessage(message, { deliverAs: "followUp" });
665
+ }
666
+ ctx.ui.notify("Queued the Broad-Side reading guide", "info");
667
+ return;
668
+ }
600
669
  const state = await ensureWorkspaceState(ctx);
601
670
  if (!state)
602
671
  return;
@@ -609,7 +678,7 @@ export default function codeCartographerExtension(pi) {
609
678
  if (!(await pathExists(skillFile))) {
610
679
  const available = await listSkillNames(state.workspaceDir);
611
680
  const hint = available.length > 0 ? ` (available: ${available.join(", ")})` : " (no skills installed)";
612
- ctx.ui.notify(`Unknown skill: ${skillName}${hint}`, "error");
681
+ 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
682
  return;
614
683
  }
615
684
  const prompt = await buildSkillPrompt(state, skillName);
@@ -624,6 +693,165 @@ export default function codeCartographerExtension(pi) {
624
693
  ctx.ui.notify(`Queued CodeCartographer skill: ${skillName}`, "info");
625
694
  },
626
695
  });
696
+ pi.registerCommand("codecarto-broadside", {
697
+ description: "Batch reconnaissance (Broad-Side): /codecarto-broadside [submit|collect|status|models] [lenses…] [flags]",
698
+ getArgumentCompletions: (prefix) => {
699
+ const items = KNOWN_BROADSIDE_TOKENS
700
+ .filter((value) => value.startsWith(prefix))
701
+ .map((value) => ({ value, label: value }));
702
+ return items.length > 0 ? items : null;
703
+ },
704
+ handler: async (args, ctx) => {
705
+ const flags = parseBroadsideFlags(args);
706
+ if (flags.unknown.length > 0) {
707
+ ctx.ui.notify(`Unknown /codecarto-broadside argument: ${flags.unknown.join(" ")}. ` +
708
+ `Actions: submit, collect, status, models. Lenses: ${BROADSIDE_LENS_IDS.join(", ")}.`, "error");
709
+ return;
710
+ }
711
+ if (flags.error) {
712
+ ctx.ui.notify(flags.error, "error");
713
+ return;
714
+ }
715
+ // Broad-Side runs on any git repository, with or without a workspace —
716
+ // so this command never goes through ensureWorkspaceState.
717
+ const broadsideDir = broadsideDirFor(ctx.cwd);
718
+ const config = await loadBroadsideConfig(broadsideDir);
719
+ const finish = (lines, notice, level = "info") => {
720
+ lastFeedbackLines = lines;
721
+ if (codecartoModeActive) {
722
+ // A workspace session already has a widget; fold the result into it.
723
+ if (ctx.hasUI)
724
+ ctx.ui.setWidget(BROADSIDE_WIDGET_ID, undefined);
725
+ void refreshWorkspaceUi(ctx, lines);
726
+ }
727
+ else if (ctx.hasUI) {
728
+ // Scout-only repository: the Broad-Side widget is the only place
729
+ // the result can live, so it holds it instead of being cleared.
730
+ ctx.ui.setWidget(BROADSIDE_WIDGET_ID, ["Broad-Side", ...lines]);
731
+ }
732
+ ctx.ui.notify(notice, level);
733
+ };
734
+ if (flags.action === "status") {
735
+ const { state } = await runBroadsideStatus(ctx.cwd);
736
+ const runs = state.runs.length;
737
+ finish(statusText(state).split("\n"), runs > 0 ? `Broad-Side: ${runs} recorded run${runs === 1 ? "" : "s"}` : "Broad-Side: no runs recorded yet");
738
+ return;
739
+ }
740
+ const apiKey = resolveBroadsideKey(config.apiKey);
741
+ if (!apiKey) {
742
+ ctx.ui.notify("No OpenRouter API key. Set OPENROUTER_API_KEY in the environment, or add api_key to " +
743
+ ".codecarto/broadside/config.yaml. (A slash command takes no key: it would land in the transcript.)", "error");
744
+ return;
745
+ }
746
+ if (flags.action === "models") {
747
+ ctx.ui.notify("Fetching the OpenRouter batch-model catalog…", "info");
748
+ try {
749
+ const { entries, benchmarks } = await listBatchModels(broadsideDir, config, apiKey, {
750
+ includeBenchmarks: flags.benchmarks,
751
+ });
752
+ finish(modelsText(entries, { benchmarks, defaultModel: config.model }).split("\n"), `Broad-Side: ${entries.length} batch model${entries.length === 1 ? "" : "s"} listed`);
753
+ }
754
+ catch (error) {
755
+ ctx.ui.notify(`Model catalog lookup failed: ${error instanceof Error ? error.message : String(error)}`, "error");
756
+ }
757
+ return;
758
+ }
759
+ // Live per-lens progress. Poll callbacks fire often, so they render
760
+ // into a widget rather than a notification stream.
761
+ const progress = new Map();
762
+ const renderProgress = (heading) => {
763
+ if (!ctx.hasUI)
764
+ return;
765
+ ctx.ui.setWidget(BROADSIDE_WIDGET_ID, [
766
+ "Broad-Side",
767
+ heading,
768
+ ...[...progress.entries()].map(([lensId, line]) => ` ${lensId}: ${line}`),
769
+ ]);
770
+ };
771
+ const onStatus = (lensId, status, counts) => {
772
+ progress.set(lensId, `${status} (${counts.completed ?? 0}/${counts.total ?? "?"})`);
773
+ renderProgress("Polling batches…");
774
+ };
775
+ const waitSeconds = flags.waitSeconds ?? config.waitSeconds;
776
+ const waitMs = waitSeconds > 0 ? waitSeconds * 1000 : undefined;
777
+ const includeSynthesis = flags.includeSynthesis ?? config.includeSynthesis;
778
+ const includeTriage = flags.includeTriage ?? config.includeTriage;
779
+ const retryTruncated = flags.retryTruncated ?? config.retryTruncated;
780
+ if (flags.action === "submit") {
781
+ const lenses = flags.lenses.length > 0 ? flags.lenses : config.defaultLenses;
782
+ renderProgress("Slicing the repository and pricing the run…");
783
+ let submit;
784
+ try {
785
+ submit = await runBroadsideSubmit(ctx.cwd, apiKey, {
786
+ lenses,
787
+ model: config.model,
788
+ maxCost: flags.maxCost ?? config.maxCost,
789
+ incremental: flags.incremental || config.incremental,
790
+ // Pi can ask, so it asks instead of refusing over max_cost the
791
+ // way MCP has to. An approval here IS the force flag.
792
+ confirm: (estimate) => ctx.ui.confirm(`Broad-Side will spend about $${estimate.totalCost.toFixed(4)}`, describeBroadsideEstimate(estimate)),
793
+ });
794
+ }
795
+ catch (error) {
796
+ if (ctx.hasUI)
797
+ ctx.ui.setWidget(BROADSIDE_WIDGET_ID, undefined);
798
+ if (error instanceof BroadsideCancelledError) {
799
+ ctx.ui.notify("Broad-Side cancelled. Nothing was submitted.", "info");
800
+ return;
801
+ }
802
+ ctx.ui.notify(`Broad-Side submit failed: ${error instanceof Error ? error.message : String(error)}`, "error");
803
+ return;
804
+ }
805
+ const lines = estimateSubmitText(submit, lenses.map(getLens)).split("\n");
806
+ if (!waitMs) {
807
+ lines.push("", "Batches are in flight. Run /codecarto-broadside collect when they finish.");
808
+ finish(lines, `Broad-Side submitted: run ${submit.runId} (~$${submit.estimatedTotalCost.toFixed(4)})`);
809
+ return;
810
+ }
811
+ ctx.ui.notify(`Broad-Side submitted run ${submit.runId}; polling for up to ${waitSeconds}s…`, "info");
812
+ try {
813
+ const collect = await runBroadsideCollect(ctx.cwd, apiKey, {
814
+ waitMs,
815
+ includeSynthesis,
816
+ includeTriage,
817
+ retryTruncated,
818
+ onStatus,
819
+ });
820
+ finish([...lines, "", ...collectResultText(collect).split("\n")], `Broad-Side ${collect.status}: run ${collect.runId}`);
821
+ }
822
+ catch (error) {
823
+ if (ctx.hasUI)
824
+ ctx.ui.setWidget(BROADSIDE_WIDGET_ID, undefined);
825
+ // The batches are submitted and paid for either way — say so, so
826
+ // nobody re-submits a run that is already in flight.
827
+ ctx.ui.notify(`Broad-Side submitted run ${submit.runId}, but collect failed: ` +
828
+ `${error instanceof Error ? error.message : String(error)}. Retry with /codecarto-broadside collect.`, "error");
829
+ }
830
+ return;
831
+ }
832
+ // action === "collect"
833
+ renderProgress("Polling batches…");
834
+ try {
835
+ const collect = await runBroadsideCollect(ctx.cwd, apiKey, {
836
+ waitMs,
837
+ includeSynthesis,
838
+ includeTriage,
839
+ retryTruncated,
840
+ onStatus,
841
+ });
842
+ const lines = collectResultText(collect).split("\n");
843
+ const done = collect.status === "completed";
844
+ if (!done)
845
+ lines.push("", "Still in flight. Run /codecarto-broadside collect again to resume.");
846
+ finish(lines, `Broad-Side ${collect.status}: ${collect.resultCount} result${collect.resultCount === 1 ? "" : "s"} saved`, done ? "info" : "warning");
847
+ }
848
+ catch (error) {
849
+ if (ctx.hasUI)
850
+ ctx.ui.setWidget(BROADSIDE_WIDGET_ID, undefined);
851
+ ctx.ui.notify(`Broad-Side collect failed: ${error instanceof Error ? error.message : String(error)}`, "error");
852
+ }
853
+ },
854
+ });
627
855
  pi.registerCommand("codecarto-publish", {
628
856
  description: "Publish the completed reimplementation spec to the configured CodeCartographer library",
629
857
  handler: async (_args, ctx) => {
@@ -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<{