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
@@ -1,5 +1,7 @@
1
- // CodeCartographer MCP server. Exposes the framework as seven JSON-RPC tools
2
- // equivalent to the seven /codecarto-* commands the Pi extension registers.
1
+ // CodeCartographer MCP server. Exposes the framework's workflow, library, and
2
+ // Broad-Side operations as JSON-RPC tools (the TOOLS array below is the
3
+ // authoritative list; the README's tool table maps each tool to its Pi
4
+ // equivalent or marks it MCP-only).
3
5
  // Both wrappers import their primitives from ../core/index.ts so phase prompts,
4
6
  // status normalization, validation, and atomic completion are byte-identical
5
7
  // across surfaces.
@@ -13,9 +15,9 @@
13
15
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
14
16
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
17
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
16
- import { cp, mkdir, readFile, rename, writeFile } from "node:fs/promises";
18
+ import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
17
19
  import { basename, isAbsolute, join } from "node:path";
18
- import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, completeValidatedPhase, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, describeScaffoldStaleness, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, isWithinPathResolved, listEntries, listGuideTopics, readGuide, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, PACKAGE_VERSION, packagedWorkspaceDir, pathExists, PhasePreflightError, publishEntry, reindex as libraryReindex, refreshScaffold, resolvePhase, resolvePipelineChoice, seedOrchestratorFiles, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../core/index.js";
20
+ import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, BROADSIDE_DIR, broadsideDirFor, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, canonicalPath, copyPackagedWorkspace, collectResultText, completeValidatedPhase, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, describeScaffoldStaleness, estimateSubmitText, getLens, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, isWithinPathResolved, listBatchModels, listEntries, listGuideTopics, loadBroadsideConfig, modelsText, readGuide, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, PACKAGE_VERSION, packagedWorkspaceDir, pathExists, PhasePreflightError, publishEntry, reindex as libraryReindex, refreshScaffold, resolvePhase, resolvePipelineChoice, readBroadsideSkill, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, seedOrchestratorFiles, statusText, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../core/index.js";
19
21
  import { applyAmendment } from "../core/amendment.js";
20
22
  import { appendUsageRun } from "../core/usage.js";
21
23
  import { initLibrary } from "../core/library.js";
@@ -84,7 +86,16 @@ export async function handleInit(args) {
84
86
  normalizeForComparison(await canonicalPath(targetWorkspaceDir)) ===
85
87
  normalizeForComparison(await canonicalPath(packagedWorkspaceDir));
86
88
  }
89
+ // Broad-Side (batch reconnaissance) creates .codecarto/broadside/ on any
90
+ // repo, workspace or not. A .codecarto/ holding only that directory is not
91
+ // an existing workspace — init must proceed and merge the template into it
92
+ // rather than demanding force and a backup of pure scout state.
93
+ let broadsideOnly = false;
87
94
  if (targetExists && !sameWorkspace) {
95
+ const entries = (await readdir(targetWorkspaceDir)).filter((entry) => entry !== BROADSIDE_DIR);
96
+ broadsideOnly = entries.length === 0 && (await pathExists(join(targetWorkspaceDir, BROADSIDE_DIR)));
97
+ }
98
+ if (targetExists && !sameWorkspace && !broadsideOnly) {
88
99
  if (!args.force) {
89
100
  throw new McpError(ErrorCode.InvalidRequest, `A .codecarto/ directory already exists at ${targetWorkspaceDir}. Pass force: true to back it up and reinitialize. Warning: this moves all existing findings, handoffs, usage data, closeouts, and phase progress to a .codecarto-backup-TIMESTAMP/ directory.`);
90
101
  }
@@ -93,7 +104,12 @@ export async function handleInit(args) {
93
104
  }
94
105
  if (!(await pathExists(targetWorkspaceDir))) {
95
106
  await mkdir(cwd, { recursive: true });
96
- await cp(packagedWorkspaceDir, targetWorkspaceDir, { recursive: true });
107
+ await copyPackagedWorkspace(targetWorkspaceDir);
108
+ }
109
+ else if (broadsideOnly) {
110
+ // Merge the template into the scout-only .codecarto/, preserving the
111
+ // broadside state and results already on disk.
112
+ await copyPackagedWorkspace(targetWorkspaceDir);
97
113
  }
98
114
  const statusPath = join(targetWorkspaceDir, "workflow", "status.yaml");
99
115
  const rawStatus = (await loadYamlFile(statusPath)) ?? {};
@@ -144,7 +160,12 @@ export async function handleStatus(args) {
144
160
  `Open questions (terminal unresolved): ${terminalOpenQuestions}`,
145
161
  `Carry-forward (pipeline phases): ${totalCarryForward}`,
146
162
  `Post-pipeline work: ${postPipelinePending} pending`,
147
- `Next: ${state.status.next_actions[0] ?? (nextPhase ? `Begin ${nextPhase.id}` : "All phases complete.")}`,
163
+ // Render every stored action: the terminal list routes to several
164
+ // post-pipeline surfaces (issue #114), and a text-reading client that
165
+ // only ever sees actions[0] loses exactly the routing it exists for.
166
+ ...(state.status.next_actions.length > 0
167
+ ? state.status.next_actions.map((action, index) => `${index === 0 ? "Next: " : " "}${action}`)
168
+ : [`Next: ${nextPhase ? `Begin ${nextPhase.id}` : "All phases complete."}`]),
148
169
  ];
149
170
  if (scaffoldNotice)
150
171
  summaryLines.push(`Scaffold: ${scaffoldNotice}`);
@@ -267,6 +288,11 @@ export async function handleComplete(args) {
267
288
  // state is already written, so a usage-log write failure must not fail
268
289
  // the completion result. Nothing else can act on the error here.
269
290
  }
291
+ // Dashboard freshness is a completion side effect (issue #112): the counts
292
+ // it renders change exactly here, and a stale dashboard misreports them
293
+ // confidently. writeDashboard never throws; its boolean says whether a
294
+ // fresh render actually landed, so the result only claims what happened.
295
+ const dashboardPath = (await writeDashboard(cwd, PACKAGE_VERSION)) ? ".codecarto/dashboard.html" : undefined;
270
296
  const lines = [
271
297
  `Marked ${validation.phaseId} complete (validation: ${validation.overall}).`,
272
298
  `Next phase: ${updatedState.status.current_phase}`,
@@ -275,12 +301,15 @@ export async function handleComplete(args) {
275
301
  lines.push(closeoutNotice);
276
302
  if (orchestratorCheckpoint)
277
303
  lines.push(orchestratorCheckpoint);
304
+ if (dashboardPath)
305
+ lines.push(`Dashboard refreshed: ${dashboardPath}`);
278
306
  return textResult(lines.join("\n"), {
279
307
  completedPhase: validation.phaseId,
280
308
  validation: validation.overall,
281
309
  nextPhase: updatedState.status.current_phase,
282
310
  closeoutNotice,
283
311
  orchestratorCheckpoint,
312
+ dashboardPath,
284
313
  });
285
314
  }
286
315
  export async function handleSkill(args) {
@@ -288,6 +317,15 @@ export async function handleSkill(args) {
288
317
  throw new McpError(ErrorCode.InvalidParams, "name is required");
289
318
  }
290
319
  const cwd = await validateCwd(args.cwd);
320
+ // Broad-Side is a reading guide for batch reconnaissance output, not a
321
+ // post-pipeline skill: it is useful before the pipeline starts and on a
322
+ // repository with no workspace at all, so it is served ahead of both gates.
323
+ if (args.name.trim() === BROADSIDE_SKILL_NAME) {
324
+ const skill = await readBroadsideSkill(cwd).catch((error) => {
325
+ throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
326
+ });
327
+ return textResult(skill.content, { skill: BROADSIDE_SKILL_NAME, path: skill.path, postPipeline: false });
328
+ }
291
329
  const state = await requireWorkspace(cwd);
292
330
  const nextPhase = getNextEligiblePhase(state);
293
331
  if (nextPhase) {
@@ -297,7 +335,7 @@ export async function handleSkill(args) {
297
335
  if (!(await pathExists(skillFile))) {
298
336
  const available = await listSkillNames(state.workspaceDir);
299
337
  const hint = available.length > 0 ? ` Available: ${available.join(", ")}.` : " No skills installed.";
300
- throw new McpError(ErrorCode.InvalidParams, `Unknown skill: ${args.name}.${hint}`);
338
+ throw new McpError(ErrorCode.InvalidParams, `Unknown skill: ${args.name}.${hint} The Broad-Side reading guide is served as \`${BROADSIDE_SKILL_NAME}\` and is not pipeline-gated.`);
301
339
  }
302
340
  const prompt = await buildSkillPrompt(state, args.name);
303
341
  return textResult(prompt, { skill: args.name });
@@ -495,7 +533,10 @@ export async function handlePublish(args) {
495
533
  capabilities,
496
534
  confidentiality,
497
535
  generation,
498
- }, { forceNewVersion: args.force_new_version === true });
536
+ }, {
537
+ forceNewVersion: args.force_new_version === true,
538
+ allowSourceRepoChange: args.allow_source_repo_change === true,
539
+ });
499
540
  const lines = [
500
541
  `Published ${result.namespace ? `${result.namespace}/` : ""}${result.slug} v${result.version} to ${libraryPath}`,
501
542
  result.isNewVersion ? `New version: v${result.version}` : `Metadata-only update (content hash matched v${result.version}).`,
@@ -684,7 +725,9 @@ export async function handleUsage(args) {
684
725
  export async function handleDashboard(args) {
685
726
  const cwd = await validateCwd(args.cwd);
686
727
  await requireWorkspace(cwd);
687
- await writeDashboard(cwd, PACKAGE_VERSION);
728
+ if (!(await writeDashboard(cwd, PACKAGE_VERSION))) {
729
+ throw new McpError(ErrorCode.InvalidRequest, "Dashboard render failed: the workspace state could not be gathered or .codecarto/dashboard.html is not writable.");
730
+ }
688
731
  return textResult("Dashboard regenerated: .codecarto/dashboard.html", { path: ".codecarto/dashboard.html" });
689
732
  }
690
733
  export async function handleListSkills(args) {
@@ -694,7 +737,13 @@ export async function handleListSkills(args) {
694
737
  const lines = skills.length > 0
695
738
  ? [`Available skills (${skills.length}):`, ...skills.map((s) => ` - ${s}`)]
696
739
  : ["No skills installed."];
697
- return textResult(lines.join("\n"), { skills });
740
+ // Broad-Side is listed apart from the post-pipeline set because it answers
741
+ // to codecarto_skill without the completion gate.
742
+ const broadsideAvailable = await readBroadsideSkill(cwd).then(() => true, () => false);
743
+ if (broadsideAvailable) {
744
+ lines.push("", `Also served by codecarto_skill (not pipeline-gated): ${BROADSIDE_SKILL_NAME} — how to read a Broad-Side batch reconnaissance run.`);
745
+ }
746
+ return textResult(lines.join("\n"), { skills, broadside: broadsideAvailable });
698
747
  }
699
748
  export async function handleRefreshScaffold(args) {
700
749
  const cwd = await validateCwd(args.cwd);
@@ -725,6 +774,9 @@ export async function handleAmend(args) {
725
774
  const { applied, closeoutNotice } = await applyAmendment(cwd, args.name).catch((error) => {
726
775
  throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
727
776
  });
777
+ // An amendment exists precisely to change the numbers the dashboard shows
778
+ // (issue #112); refresh it, reporting only a render that actually landed.
779
+ const dashboardPath = (await writeDashboard(cwd, PACKAGE_VERSION)) ? ".codecarto/dashboard.html" : undefined;
728
780
  const lines = [
729
781
  `Amendment applied.`,
730
782
  `Open questions closed: ${applied.openQuestionsClosed.length > 0 ? applied.openQuestionsClosed.join(", ") : "none"}`,
@@ -733,11 +785,125 @@ export async function handleAmend(args) {
733
785
  if (applied.unknownIds.length > 0)
734
786
  lines.push(`Ids that matched nothing (already closed or unknown): ${applied.unknownIds.join(", ")}`);
735
787
  lines.push(closeoutNotice);
788
+ if (dashboardPath)
789
+ lines.push(`Dashboard refreshed: ${dashboardPath}`);
736
790
  return textResult(lines.join("\n"), {
737
791
  openQuestionsClosed: applied.openQuestionsClosed,
738
792
  postPipelineClosed: applied.postPipelineClosed,
739
793
  unknownIds: applied.unknownIds,
740
794
  closeoutNotice,
795
+ dashboardPath,
796
+ });
797
+ }
798
+ // ---------- broadside (batch reconnaissance) ----------
799
+ function resolveBroadsideApiKey(explicit, config) {
800
+ if (explicit && explicit.trim())
801
+ return explicit.trim();
802
+ const fromEnv = process.env.OPENROUTER_API_KEY?.trim();
803
+ if (fromEnv)
804
+ return fromEnv;
805
+ if (config.apiKey)
806
+ return config.apiKey;
807
+ throw new McpError(ErrorCode.InvalidParams, "No OpenRouter API key found. Pass api_key, set the OPENROUTER_API_KEY environment variable, or add api_key to .codecarto/broadside/config.yaml.");
808
+ }
809
+ export async function handleBroadside(args) {
810
+ const cwd = await validateCwd(args.cwd);
811
+ const action = args.action ?? "submit";
812
+ if (!["submit", "collect", "status", "models"].includes(action)) {
813
+ throw new McpError(ErrorCode.InvalidParams, `Unknown action: ${action}. Valid actions: submit, collect, status, models.`);
814
+ }
815
+ const config = await loadBroadsideConfig(broadsideDirFor(cwd));
816
+ if (action === "status") {
817
+ const { state } = await runBroadsideStatus(cwd);
818
+ return textResult(statusText(state), { state });
819
+ }
820
+ const apiKey = resolveBroadsideApiKey(args.api_key, config);
821
+ // Every run knob resolves the same way: explicit parameter, else the repo's
822
+ // config.yaml default, else the shipped default baked into loadBroadsideConfig.
823
+ const waitSeconds = typeof args.wait_seconds === "number" && args.wait_seconds > 0
824
+ ? args.wait_seconds
825
+ : config.waitSeconds;
826
+ const waitMs = waitSeconds > 0 ? waitSeconds * 1000 : undefined;
827
+ const includeSynthesis = args.include_synthesis ?? config.includeSynthesis;
828
+ const includeTriage = args.include_triage ?? config.includeTriage;
829
+ const retryTruncated = args.retry_truncated ?? config.retryTruncated;
830
+ const incremental = args.incremental ?? config.incremental;
831
+ if (action === "models") {
832
+ const { entries, benchmarks } = await listBatchModels(broadsideDirFor(cwd), config, apiKey, {
833
+ includeBenchmarks: args.include_benchmarks === true,
834
+ }).catch((error) => {
835
+ throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
836
+ });
837
+ return textResult(modelsText(entries, { benchmarks, defaultModel: config.model }), {
838
+ models: entries,
839
+ defaultModel: config.model,
840
+ benchmarkMeta: benchmarks?.meta ?? null,
841
+ });
842
+ }
843
+ if (action === "submit") {
844
+ let lenses;
845
+ if (args.lenses && args.lenses.length > 0) {
846
+ const unknown = args.lenses.filter((l) => !BROADSIDE_LENS_IDS.includes(l));
847
+ if (unknown.length > 0) {
848
+ throw new McpError(ErrorCode.InvalidParams, `Unknown lens(es): ${unknown.join(", ")}. Valid: ${BROADSIDE_LENS_IDS.join(", ")}`);
849
+ }
850
+ lenses = args.lenses;
851
+ }
852
+ else {
853
+ lenses = config.defaultLenses;
854
+ }
855
+ const maxCost = typeof args.max_cost === "number" && args.max_cost > 0 ? args.max_cost : config.maxCost;
856
+ const result = await runBroadsideSubmit(cwd, apiKey, {
857
+ lenses,
858
+ model: config.model,
859
+ maxCost,
860
+ force: args.force === true,
861
+ incremental,
862
+ }).catch((error) => {
863
+ throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
864
+ });
865
+ const lines = [estimateSubmitText(result, lenses.map(getLens))];
866
+ if (waitMs) {
867
+ lines.push("", "Waiting for batches to complete...");
868
+ const collect = await runBroadsideCollect(cwd, apiKey, {
869
+ waitMs,
870
+ includeSynthesis,
871
+ includeTriage,
872
+ retryTruncated,
873
+ onStatus: (lensId, status, counts) => lines.push(` ${lensId}: ${status} (${counts.completed ?? 0}/${counts.total ?? "?"})`),
874
+ });
875
+ lines.push("", collectResultText(collect));
876
+ }
877
+ return textResult(lines.join("\n"), {
878
+ runId: result.runId,
879
+ outputDir: result.outputDir,
880
+ batches: result.batches,
881
+ estimatedTotalCost: result.estimatedTotalCost,
882
+ pricing: result.pricing,
883
+ maxCost: result.maxCost,
884
+ });
885
+ }
886
+ // action === "collect"
887
+ const collect = await runBroadsideCollect(cwd, apiKey, {
888
+ waitMs,
889
+ includeSynthesis,
890
+ includeTriage,
891
+ retryTruncated,
892
+ }).catch((error) => {
893
+ throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
894
+ });
895
+ return textResult(collectResultText(collect), {
896
+ runId: collect.runId,
897
+ status: collect.status,
898
+ totalCost: collect.totalCost,
899
+ resultCount: collect.resultCount,
900
+ truncatedCount: collect.truncatedCount,
901
+ retriedCount: collect.retriedCount,
902
+ lensOutcomes: collect.lensOutcomes,
903
+ synthesis: collect.synthesis,
904
+ triage: collect.triage,
905
+ topFindings: collect.topFindings,
906
+ topTriageItems: collect.topTriageItems,
741
907
  });
742
908
  }
743
909
  // ---------- tool registry ----------
@@ -832,12 +998,12 @@ const TOOLS = [
832
998
  },
833
999
  {
834
1000
  name: "codecarto_skill",
835
- description: "Return the prompt text for a post-pipeline skill (only callable after all phases are complete). Use codecarto_status to confirm completion first.",
1001
+ description: "Return the prompt text for a post-pipeline skill (only callable after all phases are complete). Use codecarto_status to confirm completion first. One name is exempt from the completion gate: \"broadside\" returns the reading guide for a Broad-Side batch reconnaissance run, which is meant to be read before or during the pipeline and works without a workspace.",
836
1002
  inputSchema: {
837
1003
  type: "object",
838
1004
  properties: {
839
1005
  cwd: { type: "string", description: "Absolute path to the target repository." },
840
- name: { type: "string", description: "Skill name (a directory under .codecarto/skills/)." },
1006
+ name: { type: "string", description: "Skill name (a directory under .codecarto/skills/), or \"broadside\" for the Broad-Side reading guide." },
841
1007
  },
842
1008
  required: ["cwd", "name"],
843
1009
  },
@@ -876,6 +1042,10 @@ const TOOLS = [
876
1042
  },
877
1043
  },
878
1044
  force_new_version: { type: "boolean" },
1045
+ allow_source_repo_change: {
1046
+ type: "boolean",
1047
+ description: "Permit publishing when the target entry already records a different source_repo. Off by default, because a mismatch usually means two projects derived the same slug and the spec would land in the wrong version history. Set only when the repository itself moved.",
1048
+ },
879
1049
  },
880
1050
  required: ["source_repo", "headline"],
881
1051
  },
@@ -983,7 +1153,7 @@ const TOOLS = [
983
1153
  },
984
1154
  {
985
1155
  name: "codecarto_list_skills",
986
- description: "List available post-pipeline skills installed in the workspace.",
1156
+ description: "List available post-pipeline skills installed in the workspace, plus the Broad-Side reading guide when it is present (that one is not pipeline-gated).",
987
1157
  inputSchema: {
988
1158
  type: "object",
989
1159
  properties: { cwd: { type: "string", description: "Absolute path to the target repository." } },
@@ -1011,6 +1181,63 @@ const TOOLS = [
1011
1181
  required: ["cwd"],
1012
1182
  },
1013
1183
  },
1184
+ {
1185
+ name: "codecarto_broadside",
1186
+ description: "Broad-Side: fire a cheap batch reconnaissance scan at a repository via the OpenRouter Batch API. Six lenses (architecture, api, security, defect, conventions, porting) run as asynchronous single-turn prompts with structured JSON schemas; results land in .codecarto/broadside/<run>/ as JSON plus markdown, with an optional cross-lens synthesis report. Works on any git repository — no CodeCartographer workspace required. Requires an OpenRouter API key (api_key param, OPENROUTER_API_KEY env var, or .codecarto/broadside/config.yaml). Findings are unverified scouting signals from a batch model, not validated claims — they tell the interactive pipeline where to look. Actions: submit (fire batches, returns batch ids and cost estimate), collect (poll to completion, save results, optionally synthesize), status (show recorded runs), models (list batch-capable models with pricing, context, output caps, structured-output support, and optional coding benchmarks).",
1187
+ inputSchema: {
1188
+ type: "object",
1189
+ properties: {
1190
+ cwd: { type: "string", description: "Absolute path to the target repository." },
1191
+ action: {
1192
+ type: "string",
1193
+ enum: ["submit", "collect", "status", "models"],
1194
+ description: "submit fires all lens batches and returns batch ids; collect polls submitted batches, saves results, and optionally runs the synthesis pass; status shows recorded runs; models lists batch-capable models with pricing and capabilities.",
1195
+ },
1196
+ lenses: {
1197
+ type: "array",
1198
+ items: { type: "string", enum: [...BROADSIDE_LENS_IDS] },
1199
+ description: "Lenses to run (submit only). Defaults to all six.",
1200
+ },
1201
+ api_key: {
1202
+ type: "string",
1203
+ description: "OpenRouter API key. Prefer the OPENROUTER_API_KEY environment variable or .codecarto/broadside/config.yaml.",
1204
+ },
1205
+ wait_seconds: {
1206
+ type: "number",
1207
+ description: "For submit: after submitting, poll up to this many seconds before returning. For collect: poll up to this many seconds before returning with partial state. Falls back to wait_seconds in .codecarto/broadside/config.yaml.",
1208
+ },
1209
+ include_synthesis: {
1210
+ type: "boolean",
1211
+ description: "Run the cross-lens synthesis pass once all lens batches complete. Falls back to include_synthesis in .codecarto/broadside/config.yaml (default true).",
1212
+ },
1213
+ include_triage: {
1214
+ type: "boolean",
1215
+ description: "Run the triage pass once all lens batches complete: turns the findings into a prioritized work order (impact × difficulty, P0-P3, effort estimates). Falls back to include_triage in .codecarto/broadside/config.yaml (default true).",
1216
+ },
1217
+ retry_truncated: {
1218
+ type: "boolean",
1219
+ description: "Re-submit lens results that came back truncated at the output token limit, once, with a doubled output cap. Falls back to retry_truncated in .codecarto/broadside/config.yaml (default true).",
1220
+ },
1221
+ max_cost: {
1222
+ type: "number",
1223
+ description: "Approximate run expense limit in USD. The submit action estimates the run cost from slice sizes and the configured model's per-token pricing (live OpenRouter lookup, cached 24h) and refuses to submit when the estimate exceeds the limit unless force is true. Falls back to max_cost in .codecarto/broadside/config.yaml.",
1224
+ },
1225
+ force: {
1226
+ type: "boolean",
1227
+ description: "Submit even when the cost estimate exceeds max_cost (default false).",
1228
+ },
1229
+ incremental: {
1230
+ type: "boolean",
1231
+ description: "Diff against the previous run's git HEAD and scan only the modules whose files changed (falls back to a full scan on a dirty tree or when no prior run exists). Falls back to incremental in .codecarto/broadside/config.yaml (default false).",
1232
+ },
1233
+ include_benchmarks: {
1234
+ type: "boolean",
1235
+ description: "For action 'models': annotate each model with its Artificial Analysis coding index (extra API call; default false).",
1236
+ },
1237
+ },
1238
+ required: ["cwd", "action"],
1239
+ },
1240
+ },
1014
1241
  ];
1015
1242
  const HANDLERS = {
1016
1243
  codecarto_amend: handleAmend,
@@ -1034,6 +1261,7 @@ const HANDLERS = {
1034
1261
  codecarto_dashboard: handleDashboard,
1035
1262
  codecarto_list_skills: handleListSkills,
1036
1263
  codecarto_guide: handleGuide,
1264
+ codecarto_broadside: handleBroadside,
1037
1265
  };
1038
1266
  export async function handleGuide(args) {
1039
1267
  const topics = await listGuideTopics();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecartographer-pi",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "mcpName": "io.github.HuginnIndustries/codecartographer",
5
5
  "description": "Turn an unfamiliar codebase into a validated reimplementation spec, then synthesize confirmed specs and a product vision into a traceable plan.",
6
6
  "type": "module",
@@ -35,6 +35,14 @@
35
35
  },
36
36
  "files": [
37
37
  ".codecarto/**/*",
38
+ "!.codecarto/BACKLOG.md",
39
+ "!.codecarto/THREAD_LOG.md",
40
+ "!.codecarto/CONVENTIONS.md",
41
+ "!.codecarto/DECISIONS.md",
42
+ "!.codecarto/closeouts/**",
43
+ "!.codecarto/broadside/**",
44
+ ".codecarto/broadside/SKILL.md",
45
+ ".codecarto/broadside/config.yaml",
38
46
  "agent-skill/**/*",
39
47
  "dist/**/*",
40
48
  "assets/logo.svg",
@@ -49,6 +57,7 @@
49
57
  "prepublishOnly": "npm run build",
50
58
  "test": "node --experimental-strip-types --disable-warning=ExperimentalWarning --test tests/*.test.mjs",
51
59
  "smoke": "node scripts/smoke-mcp.mjs",
60
+ "smoke:broadside": "node scripts/smoke-broadside.mjs",
52
61
  "demo:synthesis": "npm run build && node scripts/create-synthesis-demo.mjs"
53
62
  },
54
63
  "dependencies": {
@@ -1,184 +0,0 @@
1
- # Backlog
2
-
3
- Deferred framework improvements with rationale and source feedback files. Items here were
4
- proposed by one or more agents in the feedback corpus but were not load-bearing enough to land
5
- in the 2026-05-02 framework feedback pass. Each item is a candidate for a future pass.
6
-
7
- Format per entry: rationale + which feedback file(s) raised it + what the smallest viable form
8
- of the change would look like.
9
-
10
- ---
11
-
12
- ## B1. Spike template + first-class spike concept — SHIPPED in smallest viable form (v0.15.0, issue #101)
13
-
14
- Landed exactly as scoped: `templates/spike-report.md` (Goal / Method / Measurements / Findings / Recommended Deltas) with the `scratch/spikes/<spike-id>/<scenario>.md` convention documented in GUIDE.md and the spec-delta-application skill. No workflow machinery; spikes stay registered as `post_pipeline` entries (`kind: spike`), their findings flow to the spec through spec-delta-application and to `status.yaml` through `codecarto_amend`. Status.yaml spike tracking remains deferred until a project demonstrates the need.
15
-
16
- ---
17
-
18
- ## B2. Amendments mechanic — SHIPPED (v0.15.0, issue #99)
19
-
20
- Landed as `codecarto_amend` over `scratch/amendments/<slug>.yaml` (see `templates/amendment.yaml`): post-pipeline open-question closures and post_pipeline backlog retirement, applied to `workflow/status.yaml` under the completion lock with an amendment closeout and THREAD_LOG entry. The narrower back-amendment question ("a later phase says a prior output is wrong") remains open — a real run handled it with a correction section in the later report plus a delta-application pass, which worked; promote that pattern only if it recurs.
21
-
22
- ---
23
-
24
- ## B3. Cross-CodeCartographer-workspace references in status.yaml
25
-
26
- **Raised by:** `Agent on reimplementation spec - reimplementation spec.txt` (1 agent)
27
-
28
- **Why deferred:** Single-agent ask. The use case (one CodeCarto workspace cites another) is real but rare. Most projects have one CodeCarto workspace. Designing a citation format that works across workspaces (relative paths? URLs? content-addressed IDs?) is non-trivial.
29
-
30
- **Smallest viable form:** A documented convention for citing across workspaces using `<other-workspace-path>/<phase>/<output>.md#<anchor>` paths, without machinery. If a project demonstrates the need, formalize.
31
-
32
- ---
33
-
34
- ## B4. 1–5 coverage-depth score (alternative to PASS WITH GAPS)
35
-
36
- **Raised by:** `Agent on reimplementation spec - reimplementation spec.txt` (1 agent)
37
-
38
- **Why deferred:** Validation is currently structural (PASS / PARTIAL / FAIL with criterion-by-criterion check). A semantic depth score is a real ask but conflicts with the framework's posture that "honest output is the default" — a 1–5 score invites grade inflation in a way the binary criteria do not. Worth more thought before landing.
39
-
40
- **Smallest viable form:** Optional second column in the validation block: "Depth: 1–5" with a rubric (1 = section header only; 5 = exhaustive). Use only when the team agrees on the rubric.
41
-
42
- ---
43
-
44
- ## B5. Programmatic markdown-regex validator
45
-
46
- **Raised by:** `Agent on Deep Defect Scan - defect-scan-deep.txt`, `Agent on broad Defect Scan - defect-scan-broad.txt` (2 agents)
47
-
48
- **Why deferred:** A real validator (parses status.yaml, checks output paths, verifies validation blocks exist with correct criterion counts, etc.) is a separate tooling project — more than a SKILL/template change. It deserves its own scoping.
49
-
50
- **Smallest viable form:** A `scripts/validate.sh` that runs `yq` + `grep` checks for the most common gates (every primary_output exists; every validation block has at least N rows; every status.yaml phase has a status field). Document, don't ship without consensus on scope.
51
-
52
- ---
53
-
54
- ## B6. CI grep gate for tripwire-named functions
55
-
56
- **Raised by:** `Agent on fifth module work - thaum-state.txt` (1 agent — project-level concern)
57
-
58
- **Why deferred:** This is a *project*-level CI concern (Thaumaturge), not a framework concern. The framework's job is to surface the convention (which the orchestrator does in CONVENTIONS.md); the project's job is to gate it. Documented for the orchestrator to pick up at the project level.
59
-
60
- **Smallest viable form:** Document in CONVENTIONS.md template's example entry that "production code path importing any tripwire-named function should fail the build" is a known follow-up. No framework artifact.
61
-
62
- ---
63
-
64
- ## B7. Append-mode supersession rule (consolidate vs append)
65
-
66
- **Raised by:** `Agent on porting phase - porting.txt`, `Agent on protocols phase - protocols.txt`, `Agent on contracts phase - contracts.txt` (3 agents — at threshold but defers per scope)
67
-
68
- **Why deferred:** Real friction: secondary outputs accumulate overlapping descriptions and recency wins by unwritten convention. A "supersedes" marker would land cleanly, but it also implies a reconciliation pass before reimplementation-spec, which is a phase-shape change. Worth landing in a follow-up pass that focuses on append-mode discipline holistically.
69
-
70
- **Smallest viable form:** A `> SUPERSEDES <date>:<reason>` block convention at the top of each new dated section in append-mode files, plus a paragraph in GUIDE.md describing it. No machinery; just convention.
71
-
72
- ---
73
-
74
- ## B8. Validation as semantic check (not just structural / completeness)
75
-
76
- **Raised by:** `Agent on broad Defect Scan - defect-scan-broad.txt`, `Agent on Deep Defect Scan - defect-scan-deep.txt`, `Agent on porting phase - porting.txt`, `Agent on protocols phase - protocols.txt` (4 agents — at-threshold)
77
-
78
- **Why deferred:** "LLM grades its own homework" is a real gap. The fix is either a quality-subagent (a separate LLM pass that grades against the criteria) or a coverage-depth score (B4). Both are larger changes than this pass should land. The structural check at minimum prevents the worst failure mode (skipping the criterion entirely).
79
-
80
- **Smallest viable form:** A `quality-review` skill that delegates to a subagent for criterion-by-criterion semantic check. Optional; not in the default closeout ritual.
81
-
82
- ---
83
-
84
- ## B9. Spec file split (single 1000+ line file → spec/ directory)
85
-
86
- **Raised by:** `Agent on fourth module work - thaum-engine.txt`, `Agent on second module work - thaum-providers-ollama.txt`, `Run three Thaumaturge implementation spikes.txt` (3 agents — at-threshold)
87
-
88
- **Why deferred:** This is a *project*-level concern (Thaumaturge's reimplementation-spec.md is 1300+ lines). The framework's template doesn't enforce a single file; a project can split. Documented for the orchestrator to pick up at the project level.
89
-
90
- **Smallest viable form:** A documented convention in the opinionated reimpl-spec template that says "if the spec exceeds N lines, split into spec/ with INDEX.md anchored to original section IDs." No framework machinery.
91
-
92
- ---
93
-
94
- ## B10. SKILL.md and template files redundancy / consolidation
95
-
96
- **Raised by:** `Agent on porting phase - porting.txt`, `Agent on protocols phase - protocols.txt`, `Agent on contracts phase - contracts.txt` (3 agents — at-threshold)
97
-
98
- **Why deferred:** "SKILL says X, template says X — agent reads both and reconciles" is a real cost. But the framework's posture is that SKILL.md is the *how* (analysis instructions) and template is the *shape* (output skeleton). Collapsing them risks losing the separation. A surgical edit (de-duplicate the obvious overlaps without merging) is hard to do without per-file judgment.
99
-
100
- **Smallest viable form:** A pass that inspects each SKILL/template pair and removes section headers from the SKILL that exactly match the template (the template alone is canonical for shape). Per-file work; not a single sweep.
101
-
102
- ---
103
-
104
- ## B11. Defect-pass-N append discipline (multi-pass defect-scan)
105
-
106
- **Raised by:** `Agent on Deep Defect Scan - defect-scan-deep.txt`, `Agent on broad Defect Scan - defect-scan-broad.txt` (2 agents)
107
-
108
- **Why deferred:** Defect-scan today expects "one pass = one phase output." Multi-pass (broad → deep) is a real pattern that emerged in Thaumaturge. Partially blessed in 2026-05-04 by `pipeline-full-with-deep-audit.yaml`, which splits the scan into mechanical (passes 1, 2, 6 — after architecture, before contracts) and semantic (passes 3, 4, 5 — after protocols, before porting) phases. Different mechanism than the originally-proposed within-phase pass-N append, but it covers the broad→deep use case. Open question: do projects still need the within-phase append pattern?
109
-
110
- **Smallest viable form:** A `templates/defect-report-pass-N.md` template with a `findings/defect-scan/passes/<pass-id>.md` directory convention. Documented; not pre-applied.
111
-
112
- ---
113
-
114
- ## B12. Resolutions footer / phase resolution mechanic
115
-
116
- **Raised by:** `Agent coordinating results - coordinating.txt`, `Agent on contracts phase - contracts.txt` (2 agents)
117
-
118
- **Why deferred:** Partially resolved by `carry_forward` in this pass — the routing-and-resolution loop now exists. The remaining ask (a "previously resolved" footer in each output, or a resolutions/ directory) may be obviated by `carry_forward` in practice. Wait for next-pass feedback before adding more machinery.
119
-
120
- ---
121
-
122
- ## B13. Engine→leaf seam contracts table
123
-
124
- **Raised by:** `Agent on fifth module work - thaum-state.txt` (1 agent — project-level)
125
-
126
- **Why deferred:** Project-level (Thaumaturge-specific). The framework's job is to surface the convention; CONVENTIONS.md template now has the shape for it. The actual SEAMS.md would live at the project level.
127
-
128
- ---
129
-
130
- ## B14. Cross-phase consistency check
131
-
132
- **Raised by:** `Agent on protocols phase - protocols.txt` (1 agent)
133
-
134
- **Why deferred:** "Contracts and protocols both touch dispatcher / redaction / SSE and must stay aligned" — the proposed consistency check is a pre-reimpl-spec reconciliation pass. Related to B7 (append-mode supersession). Bundle into the same future pass.
135
-
136
- ---
137
-
138
- ## B15. spec-delta-application SKILL canonical refinements (first-run friction)
139
-
140
- **Raised by:** `Spec-delta-3 application - 2026-05-03.txt` (1 agent — first real run of the SKILL post-2026-05-02 framework pass; high-signal because the SKILL had no field exposure before this)
141
-
142
- **Why deferred:** Six small refinements to the same SKILL.md / templates pair, all visible only after a non-spike-sourced run. Bundling makes the next pass cheap; piecemeal would churn the SKILL repeatedly.
143
-
144
- **Smallest viable form:** A surgical pass over `skills/spec-delta-application/SKILL.md` and `templates/deltas-applied.md` covering:
145
- - **Citation forms** — canonicalize a small family beyond the spike-sourced `[revised per <file> §<delta-id>]`. Round-3 introduced `[revised per DECISIONS.md D5XX (Δ-N), round-N]` for D-entry-sourced deltas and `[revised per closeouts/<file> §<id>, round-N]` for closeout-sourced deltas. Document both as canonical, alongside the spike form.
146
- - **Audit file path** — explicitly cover the non-spike case. Round-3 used `findings/deltas-applied/round-N.md` (directory convention) when the deltas weren't spike-driven; the existing "sibling-of-spec DELTAS-APPLIED.md" remains a special case for spike rounds.
147
- - **Closeout filename** — round-numbered (`closeouts/<YYYY-MM-DD>-spec-delta-N.md`) avoids the collision risk of the current `closeouts/<YYYY-MM-DD>-spec-deltas.md` when two passes happen on the same day.
148
- - **§header naming** — the spec's `## Post-Spike Revisions` header doesn't generalize for non-spike rounds. Rename to `## Post-Pipeline Revisions` (or similar) so post-pipeline non-spike rounds don't have to add explanatory paragraphs about why "Post-Spike" still applies.
149
- - **Step 6 prominence** — add a sentence to Step 6 ("Update the spec's front-matter citation conventions list") emphasizing this is required when the application introduces new citation forms; otherwise the audit-table marker list is unanchored.
150
- - **APPLY may refine the proposal** — Δ-36's literal D501 text was "map to 'error'" but the application refined to "emit error event + done(error)" mirroring the existing in-stream-error pattern. The four-bucket matrix's APPLY bucket implicitly permits refinement, but the SKILL should make it explicit: APPLY may include refinements beyond the literal proposal text, with the refinement captured as a Decision-Beyond-Triage in the closeout.
151
-
152
- ---
153
-
154
- ## B16. Project-level BACKLOG.md template + GUIDE.md project-vs-framework-level clarification
155
-
156
- **Raised by:** `Spec-delta-3 application - 2026-05-03.txt` (1 agent — first-run-of-SKILL friction)
157
-
158
- **Why deferred:** The SKILL says "DEFER → Add to BACKLOG.md with rationale and a back-reference," but the codex workspace had no project-level BACKLOG.md and the upstream `.codecarto/BACKLOG.md` is for framework-feedback deferrals (correctly excluded from project sync). Round-3 created `.codecarto/BACKLOG.md` from scratch with a project-scoped shape. Future projects will hit the same bootstrap gap.
159
-
160
- **Smallest viable form:**
161
- - Ship `templates/backlog-project.md` skeleton with header + format-per-entry note + an example entry. Format-per-entry should include: rationale, raised-by (closeout file or D-entry), preconditions (which modules / artifacts need to land before the deferral can be revisited — Δ-38 demonstrated this field's value), and "smallest viable form."
162
- - Add one paragraph to `GUIDE.md` clarifying the project-level vs framework-level distinction: framework `.codecarto/BACKLOG.md` is for framework-feedback deferrals (raised by agents about CodeCartographer itself); project `.codecarto/BACKLOG.md` is for project-decision deferrals (raised during the project's own work).
163
- - SKILL Step 7 (DEFER bucket) explicitly references the project template.
164
-
165
- ---
166
-
167
- ## B17. SKILL clarification — DECISIONS.md vs BACKLOG.md semantics for DEFER
168
-
169
- **Raised by:** `Spec-delta-3 application - 2026-05-03.txt` (1 agent — first-run-of-SKILL semantic ambiguity)
170
-
171
- **Why deferred:** The SKILL's DEFER guidance and its "append numbered entries to DECISIONS.md for any decisions made during triage that weren't already in the deltas" instruction left ambiguous whether DEFER-with-rationale belongs in DECISIONS.md or BACKLOG.md. Round-3 chose: DECISIONS.md is for things the project decided to **DO** (including refinements made during application — e.g., D501's refinement); BACKLOG.md is for things the project decided to **DEFER**. DEFERs do not get a D5xx number, and DECISIONS.md's "pending spec deltas" subsection only tracks proposed → applied lifecycle.
172
-
173
- **Smallest viable form:** A two-sentence clarification in SKILL Step 7's DEFER bucket: DEFER → BACKLOG.md (no D-number); refinements made during APPLY → DECISIONS.md "Decisions Beyond Triage" section in the audit file, lifted to DECISIONS.md only if cross-cutting. Existing D-entries for proposed deltas get their disposition updated in place (e.g., "APPLIED 2026-05-03 round-3"); they do not get superseded by new D-entries when applied.
174
-
175
- ---
176
-
177
- ## How to use this backlog
178
-
179
- A future framework-feedback pass picks items up from here. Each item has a "Smallest viable
180
- form" line so the pass doesn't have to re-design from scratch — the design work was done in this
181
- pass; the next pass executes.
182
-
183
- Add new entries below as agents raise items in future feedback files. When promoting an item to
184
- "applied," remove the entry from this file and document in the relevant CHANGELOG.