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,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, detectProvenanceConflicts, 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)) ?? {};
@@ -246,7 +262,7 @@ export async function handleComplete(args) {
246
262
  if (validation.overall === "FAIL" || validation.overall === "MISSING") {
247
263
  throw new McpError(ErrorCode.InvalidRequest, `Cannot complete ${validation.phaseId}: validation is ${validation.overall}.\n${buildValidationSummary(validation).join("\n")}`);
248
264
  }
249
- const { updatedState, closeoutNotice, orchestratorCheckpoint } = await completeValidatedPhase(cwd, validation, "codecarto_complete").catch((error) => {
265
+ const { updatedState, closeoutNotice, orchestratorCheckpoint, warnings } = await completeValidatedPhase(cwd, validation, "codecarto_complete").catch((error) => {
250
266
  throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error));
251
267
  });
252
268
  // Record the run in the usage log (issue #100). MCP hosts execute phases in
@@ -287,6 +303,10 @@ export async function handleComplete(args) {
287
303
  lines.push(orchestratorCheckpoint);
288
304
  if (dashboardPath)
289
305
  lines.push(`Dashboard refreshed: ${dashboardPath}`);
306
+ for (const warning of validation.warnings ?? [])
307
+ lines.push(`NOTE: ${warning} Non-gating.`);
308
+ for (const warning of warnings)
309
+ lines.push(`NOTE: ${warning} Non-gating.`);
290
310
  return textResult(lines.join("\n"), {
291
311
  completedPhase: validation.phaseId,
292
312
  validation: validation.overall,
@@ -294,6 +314,7 @@ export async function handleComplete(args) {
294
314
  closeoutNotice,
295
315
  orchestratorCheckpoint,
296
316
  dashboardPath,
317
+ warnings: [...(validation.warnings ?? []), ...warnings],
297
318
  });
298
319
  }
299
320
  export async function handleSkill(args) {
@@ -301,6 +322,15 @@ export async function handleSkill(args) {
301
322
  throw new McpError(ErrorCode.InvalidParams, "name is required");
302
323
  }
303
324
  const cwd = await validateCwd(args.cwd);
325
+ // Broad-Side is a reading guide for batch reconnaissance output, not a
326
+ // post-pipeline skill: it is useful before the pipeline starts and on a
327
+ // repository with no workspace at all, so it is served ahead of both gates.
328
+ if (args.name.trim() === BROADSIDE_SKILL_NAME) {
329
+ const skill = await readBroadsideSkill(cwd).catch((error) => {
330
+ throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
331
+ });
332
+ return textResult(skill.content, { skill: BROADSIDE_SKILL_NAME, path: skill.path, postPipeline: false });
333
+ }
304
334
  const state = await requireWorkspace(cwd);
305
335
  const nextPhase = getNextEligiblePhase(state);
306
336
  if (nextPhase) {
@@ -310,7 +340,7 @@ export async function handleSkill(args) {
310
340
  if (!(await pathExists(skillFile))) {
311
341
  const available = await listSkillNames(state.workspaceDir);
312
342
  const hint = available.length > 0 ? ` Available: ${available.join(", ")}.` : " No skills installed.";
313
- throw new McpError(ErrorCode.InvalidParams, `Unknown skill: ${args.name}.${hint}`);
343
+ 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.`);
314
344
  }
315
345
  const prompt = await buildSkillPrompt(state, args.name);
316
346
  return textResult(prompt, { skill: args.name });
@@ -447,6 +477,33 @@ async function resolveDefaultsFromWorkspace(cwd, overrides) {
447
477
  }
448
478
  return { pipeline, namespace };
449
479
  }
480
+ // ---------- library provenance reporting ----------
481
+ function conflictEntryLabel(conflict) {
482
+ return conflict.namespace ? `${conflict.namespace}/${conflict.slug}` : conflict.slug;
483
+ }
484
+ /**
485
+ * The report block appended to the list and reindex text results when an
486
+ * entry's versions disagree about source_repo (#148). Empty when there is
487
+ * nothing to report, so a healthy library's output does not change. Repair
488
+ * is manual by design: the framework does not rename or renumber versions,
489
+ * because entry paths are ABI.
490
+ */
491
+ function provenanceConflictLines(conflicts) {
492
+ if (conflicts.length === 0)
493
+ return [];
494
+ const lines = [
495
+ "",
496
+ `Provenance conflicts — ${conflicts.length} ${conflicts.length === 1 ? "entry" : "entries"} whose versions disagree about source_repo:`,
497
+ ];
498
+ for (const conflict of conflicts) {
499
+ const others = conflict.disagreeing_versions
500
+ .map((v) => `v${v.version} records ${v.source_repo}`)
501
+ .join("; ");
502
+ lines.push(` ${conflictEntryLabel(conflict)}: the index advertises ${conflict.source_repo} (v${conflict.latest_version}), but ${others}.`);
503
+ }
504
+ lines.push("These entries were merged by a slug collision before publish refused cross-project appends, so each version history spans more than one codebase (a repository that genuinely moved and was re-published with allow_source_repo_change leaves the same shape).", "Repair is manual: split the entry by hand — the framework does not rename or renumber versions, because entry paths are ABI.");
505
+ return lines;
506
+ }
450
507
  // ---------- library handlers ----------
451
508
  export async function handlePublish(args) {
452
509
  const libraryPath = await resolveLibraryPath(args);
@@ -508,7 +565,11 @@ export async function handlePublish(args) {
508
565
  capabilities,
509
566
  confidentiality,
510
567
  generation,
511
- }, { forceNewVersion: args.force_new_version === true });
568
+ }, {
569
+ forceNewVersion: args.force_new_version === true,
570
+ allowSourceRepoChange: args.allow_source_repo_change === true,
571
+ allowConfidentialityMismatch: args.allow_confidentiality_mismatch === true,
572
+ });
512
573
  const lines = [
513
574
  `Published ${result.namespace ? `${result.namespace}/` : ""}${result.slug} v${result.version} to ${libraryPath}`,
514
575
  result.isNewVersion ? `New version: v${result.version}` : `Metadata-only update (content hash matched v${result.version}).`,
@@ -539,6 +600,10 @@ export async function handleLibraryList(args) {
539
600
  if (typeof args.source_repo === "string" && args.source_repo !== "")
540
601
  filter.source_repo = args.source_repo;
541
602
  const entries = await listEntries(libraryPath, filter);
603
+ // Computed over the listed entries only, read-only: one readdir per entry
604
+ // plus one small metadata read per older version.
605
+ const conflicts = await detectProvenanceConflicts(libraryPath, entries);
606
+ const conflicted = new Set(conflicts.map((c) => `${c.namespace ?? ""}/${c.slug}`));
542
607
  const summary = entries.length === 0
543
608
  ? `No entries match the filter in ${libraryPath}.`
544
609
  : [
@@ -546,8 +611,10 @@ export async function handleLibraryList(args) {
546
611
  ...entries.map((e) => {
547
612
  const ns = e.namespace ? `${e.namespace}/` : "";
548
613
  const tags = e.tags.length > 0 ? ` [${e.tags.slice(0, 4).join(", ")}${e.tags.length > 4 ? ", ..." : ""}]` : "";
549
- return ` ${ns}${e.slug} v${e.latest_version} — ${e.headline}${tags}`;
614
+ const flag = conflicted.has(`${e.namespace ?? ""}/${e.slug}`) ? " — PROVENANCE CONFLICT (see below)" : "";
615
+ return ` ${ns}${e.slug} v${e.latest_version} — ${e.headline}${tags}${flag}`;
550
616
  }),
617
+ ...provenanceConflictLines(conflicts),
551
618
  ].join("\n");
552
619
  return textResult(summary, {
553
620
  libraryPath,
@@ -555,6 +622,7 @@ export async function handleLibraryList(args) {
555
622
  namespaced: marker.namespaced,
556
623
  count: entries.length,
557
624
  entries,
625
+ provenance_conflicts: conflicts,
558
626
  });
559
627
  }
560
628
  export async function handleLibraryReindex(args) {
@@ -565,17 +633,27 @@ export async function handleLibraryReindex(args) {
565
633
  }
566
634
  const index = await libraryReindex(libraryPath);
567
635
  const namespaces = index.namespaces.length > 0 ? index.namespaces.join(", ") : "(none)";
568
- return textResult(`Reindexed ${libraryPath}: ${index.entry_count} ${index.entry_count === 1 ? "entry" : "entries"} across namespaces [${namespaces}].`, {
636
+ return textResult([
637
+ `Reindexed ${libraryPath}: ${index.entry_count} ${index.entry_count === 1 ? "entry" : "entries"} across namespaces [${namespaces}].`,
638
+ ...provenanceConflictLines(index.provenance_conflicts),
639
+ ].join("\n"), {
569
640
  libraryPath,
570
641
  libraryName: index.library_name,
571
642
  entry_count: index.entry_count,
572
643
  namespaces: index.namespaces,
644
+ provenance_conflicts: index.provenance_conflicts,
573
645
  });
574
646
  }
575
647
  export async function handleLibraryInit(args) {
576
648
  if (!args.library_path || typeof args.library_path !== "string") {
577
649
  throw new McpError(ErrorCode.InvalidParams, "library_path is required.");
578
650
  }
651
+ // Same rule as resolveLibraryPath for the other library tools: a relative
652
+ // path would resolve against the MCP server process's cwd and then be
653
+ // persisted verbatim into the user-global config (#134).
654
+ if (!isAbsolute(args.library_path)) {
655
+ throw new McpError(ErrorCode.InvalidParams, `library_path must be absolute, got: ${args.library_path}`);
656
+ }
579
657
  const libraryPath = args.library_path;
580
658
  const namespaced = !!args.namespace;
581
659
  const result = await initLibrary(libraryPath, {
@@ -709,7 +787,13 @@ export async function handleListSkills(args) {
709
787
  const lines = skills.length > 0
710
788
  ? [`Available skills (${skills.length}):`, ...skills.map((s) => ` - ${s}`)]
711
789
  : ["No skills installed."];
712
- return textResult(lines.join("\n"), { skills });
790
+ // Broad-Side is listed apart from the post-pipeline set because it answers
791
+ // to codecarto_skill without the completion gate.
792
+ const broadsideAvailable = await readBroadsideSkill(cwd).then(() => true, () => false);
793
+ if (broadsideAvailable) {
794
+ lines.push("", `Also served by codecarto_skill (not pipeline-gated): ${BROADSIDE_SKILL_NAME} — how to read a Broad-Side batch reconnaissance run.`);
795
+ }
796
+ return textResult(lines.join("\n"), { skills, broadside: broadsideAvailable });
713
797
  }
714
798
  export async function handleRefreshScaffold(args) {
715
799
  const cwd = await validateCwd(args.cwd);
@@ -761,6 +845,117 @@ export async function handleAmend(args) {
761
845
  dashboardPath,
762
846
  });
763
847
  }
848
+ // ---------- broadside (batch reconnaissance) ----------
849
+ function resolveBroadsideApiKey(explicit, config) {
850
+ if (explicit && explicit.trim())
851
+ return explicit.trim();
852
+ const fromEnv = process.env.OPENROUTER_API_KEY?.trim();
853
+ if (fromEnv)
854
+ return fromEnv;
855
+ if (config.apiKey)
856
+ return config.apiKey;
857
+ 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.");
858
+ }
859
+ export async function handleBroadside(args) {
860
+ const cwd = await validateCwd(args.cwd);
861
+ const action = args.action ?? "submit";
862
+ if (!["submit", "collect", "status", "models"].includes(action)) {
863
+ throw new McpError(ErrorCode.InvalidParams, `Unknown action: ${action}. Valid actions: submit, collect, status, models.`);
864
+ }
865
+ const config = await loadBroadsideConfig(broadsideDirFor(cwd));
866
+ if (action === "status") {
867
+ const { state } = await runBroadsideStatus(cwd);
868
+ return textResult(statusText(state), { state });
869
+ }
870
+ const apiKey = resolveBroadsideApiKey(args.api_key, config);
871
+ // Every run knob resolves the same way: explicit parameter, else the repo's
872
+ // config.yaml default, else the shipped default baked into loadBroadsideConfig.
873
+ const waitSeconds = typeof args.wait_seconds === "number" && args.wait_seconds > 0
874
+ ? args.wait_seconds
875
+ : config.waitSeconds;
876
+ const waitMs = waitSeconds > 0 ? waitSeconds * 1000 : undefined;
877
+ const includeSynthesis = args.include_synthesis ?? config.includeSynthesis;
878
+ const includeTriage = args.include_triage ?? config.includeTriage;
879
+ const retryTruncated = args.retry_truncated ?? config.retryTruncated;
880
+ const incremental = args.incremental ?? config.incremental;
881
+ if (action === "models") {
882
+ const { entries, benchmarks } = await listBatchModels(broadsideDirFor(cwd), config, apiKey, {
883
+ includeBenchmarks: args.include_benchmarks === true,
884
+ }).catch((error) => {
885
+ throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
886
+ });
887
+ return textResult(modelsText(entries, { benchmarks, defaultModel: config.model }), {
888
+ models: entries,
889
+ defaultModel: config.model,
890
+ benchmarkMeta: benchmarks?.meta ?? null,
891
+ });
892
+ }
893
+ if (action === "submit") {
894
+ let lenses;
895
+ if (args.lenses && args.lenses.length > 0) {
896
+ const unknown = args.lenses.filter((l) => !BROADSIDE_LENS_IDS.includes(l));
897
+ if (unknown.length > 0) {
898
+ throw new McpError(ErrorCode.InvalidParams, `Unknown lens(es): ${unknown.join(", ")}. Valid: ${BROADSIDE_LENS_IDS.join(", ")}`);
899
+ }
900
+ lenses = args.lenses;
901
+ }
902
+ else {
903
+ lenses = config.defaultLenses;
904
+ }
905
+ const maxCost = typeof args.max_cost === "number" && args.max_cost > 0 ? args.max_cost : config.maxCost;
906
+ const result = await runBroadsideSubmit(cwd, apiKey, {
907
+ lenses,
908
+ model: config.model,
909
+ maxCost,
910
+ force: args.force === true,
911
+ incremental,
912
+ }).catch((error) => {
913
+ throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
914
+ });
915
+ const lines = [estimateSubmitText(result, lenses.map(getLens))];
916
+ if (waitMs) {
917
+ lines.push("", "Waiting for batches to complete...");
918
+ const collect = await runBroadsideCollect(cwd, apiKey, {
919
+ waitMs,
920
+ includeSynthesis,
921
+ includeTriage,
922
+ retryTruncated,
923
+ onStatus: (lensId, status, counts) => lines.push(` ${lensId}: ${status} (${counts.completed ?? 0}/${counts.total ?? "?"})`),
924
+ });
925
+ lines.push("", collectResultText(collect));
926
+ }
927
+ return textResult(lines.join("\n"), {
928
+ runId: result.runId,
929
+ outputDir: result.outputDir,
930
+ batches: result.batches,
931
+ estimatedTotalCost: result.estimatedTotalCost,
932
+ pricing: result.pricing,
933
+ maxCost: result.maxCost,
934
+ });
935
+ }
936
+ // action === "collect"
937
+ const collect = await runBroadsideCollect(cwd, apiKey, {
938
+ waitMs,
939
+ includeSynthesis,
940
+ includeTriage,
941
+ retryTruncated,
942
+ }).catch((error) => {
943
+ throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
944
+ });
945
+ return textResult(collectResultText(collect), {
946
+ runId: collect.runId,
947
+ status: collect.status,
948
+ totalCost: collect.totalCost,
949
+ resultCount: collect.resultCount,
950
+ truncatedCount: collect.truncatedCount,
951
+ retriedCount: collect.retriedCount,
952
+ lensOutcomes: collect.lensOutcomes,
953
+ synthesis: collect.synthesis,
954
+ triage: collect.triage,
955
+ topFindings: collect.topFindings,
956
+ topTriageItems: collect.topTriageItems,
957
+ });
958
+ }
764
959
  // ---------- tool registry ----------
765
960
  const TOOLS = [
766
961
  {
@@ -853,12 +1048,12 @@ const TOOLS = [
853
1048
  },
854
1049
  {
855
1050
  name: "codecarto_skill",
856
- description: "Return the prompt text for a post-pipeline skill (only callable after all phases are complete). Use codecarto_status to confirm completion first.",
1051
+ 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.",
857
1052
  inputSchema: {
858
1053
  type: "object",
859
1054
  properties: {
860
1055
  cwd: { type: "string", description: "Absolute path to the target repository." },
861
- name: { type: "string", description: "Skill name (a directory under .codecarto/skills/)." },
1056
+ name: { type: "string", description: "Skill name (a directory under .codecarto/skills/), or \"broadside\" for the Broad-Side reading guide." },
862
1057
  },
863
1058
  required: ["cwd", "name"],
864
1059
  },
@@ -884,7 +1079,11 @@ const TOOLS = [
884
1079
  headline: { type: "string" },
885
1080
  tags: { type: "array", items: { type: "string" } },
886
1081
  capabilities: { type: "array", items: { type: "string" } },
887
- confidentiality: { type: "string", enum: ["internal", "shared", "public"] },
1082
+ confidentiality: {
1083
+ type: "string",
1084
+ enum: ["internal", "shared", "public"],
1085
+ description: "Classification of the entry. Defaults to internal. Ordered internal < shared < public; publish refuses an entry more restricted than the library's visibility unless allow_confidentiality_mismatch is set.",
1086
+ },
888
1087
  model_metadata: {
889
1088
  type: "object",
890
1089
  properties: {
@@ -897,13 +1096,21 @@ const TOOLS = [
897
1096
  },
898
1097
  },
899
1098
  force_new_version: { type: "boolean" },
1099
+ allow_source_repo_change: {
1100
+ type: "boolean",
1101
+ 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.",
1102
+ },
1103
+ allow_confidentiality_mismatch: {
1104
+ type: "boolean",
1105
+ description: "Permit publishing when the entry's confidentiality is more restricted than the library's visibility (internal < shared < public: an internal entry into a shared or public library, a shared entry into a public one). Off by default, because that direction exposes the spec to everyone the library reaches. An omitted confidentiality counts as internal. Set only when the exposure is intended; the recorded confidentiality is not changed.",
1106
+ },
900
1107
  },
901
1108
  required: ["source_repo", "headline"],
902
1109
  },
903
1110
  },
904
1111
  {
905
1112
  name: "codecarto_library_list",
906
- description: "List entries in a CodeCartographer library, optionally filtered by namespace, tag, slug, or source_repo. The library is identified by library_path (absolute) or by cwd's config.yaml.",
1113
+ description: "List entries in a CodeCartographer library, optionally filtered by namespace, tag, slug, or source_repo. The library is identified by library_path (absolute) or by cwd's config.yaml. Flags entries whose versions disagree about source_repo (merged by a slug collision before publish refused cross-project appends); repair is manual.",
907
1114
  inputSchema: {
908
1115
  type: "object",
909
1116
  properties: {
@@ -918,7 +1125,7 @@ const TOOLS = [
918
1125
  },
919
1126
  {
920
1127
  name: "codecarto_library_reindex",
921
- description: "Regenerate index.yaml and INDEX.md for a CodeCartographer library from filesystem state. Use after manual edits or to resolve a git merge conflict on index.yaml.",
1128
+ description: "Regenerate index.yaml and INDEX.md for a CodeCartographer library from filesystem state. Use after manual edits or to resolve a git merge conflict on index.yaml. Also reports entries whose versions disagree about source_repo (merged by a slug collision before publish refused cross-project appends); the index files are not changed and repair is manual.",
922
1129
  inputSchema: {
923
1130
  type: "object",
924
1131
  properties: {
@@ -1004,7 +1211,7 @@ const TOOLS = [
1004
1211
  },
1005
1212
  {
1006
1213
  name: "codecarto_list_skills",
1007
- description: "List available post-pipeline skills installed in the workspace.",
1214
+ 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).",
1008
1215
  inputSchema: {
1009
1216
  type: "object",
1010
1217
  properties: { cwd: { type: "string", description: "Absolute path to the target repository." } },
@@ -1032,6 +1239,63 @@ const TOOLS = [
1032
1239
  required: ["cwd"],
1033
1240
  },
1034
1241
  },
1242
+ {
1243
+ name: "codecarto_broadside",
1244
+ 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).",
1245
+ inputSchema: {
1246
+ type: "object",
1247
+ properties: {
1248
+ cwd: { type: "string", description: "Absolute path to the target repository." },
1249
+ action: {
1250
+ type: "string",
1251
+ enum: ["submit", "collect", "status", "models"],
1252
+ 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.",
1253
+ },
1254
+ lenses: {
1255
+ type: "array",
1256
+ items: { type: "string", enum: [...BROADSIDE_LENS_IDS] },
1257
+ description: "Lenses to run (submit only). Defaults to all six.",
1258
+ },
1259
+ api_key: {
1260
+ type: "string",
1261
+ description: "OpenRouter API key. Prefer the OPENROUTER_API_KEY environment variable or .codecarto/broadside/config.yaml.",
1262
+ },
1263
+ wait_seconds: {
1264
+ type: "number",
1265
+ 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.",
1266
+ },
1267
+ include_synthesis: {
1268
+ type: "boolean",
1269
+ description: "Run the cross-lens synthesis pass once all lens batches complete. Falls back to include_synthesis in .codecarto/broadside/config.yaml (default true).",
1270
+ },
1271
+ include_triage: {
1272
+ type: "boolean",
1273
+ 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).",
1274
+ },
1275
+ retry_truncated: {
1276
+ type: "boolean",
1277
+ 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).",
1278
+ },
1279
+ max_cost: {
1280
+ type: "number",
1281
+ 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.",
1282
+ },
1283
+ force: {
1284
+ type: "boolean",
1285
+ description: "Submit even when the cost estimate exceeds max_cost (default false).",
1286
+ },
1287
+ incremental: {
1288
+ type: "boolean",
1289
+ 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).",
1290
+ },
1291
+ include_benchmarks: {
1292
+ type: "boolean",
1293
+ description: "For action 'models': annotate each model with its Artificial Analysis coding index (extra API call; default false).",
1294
+ },
1295
+ },
1296
+ required: ["cwd", "action"],
1297
+ },
1298
+ },
1035
1299
  ];
1036
1300
  const HANDLERS = {
1037
1301
  codecarto_amend: handleAmend,
@@ -1055,6 +1319,7 @@ const HANDLERS = {
1055
1319
  codecarto_dashboard: handleDashboard,
1056
1320
  codecarto_list_skills: handleListSkills,
1057
1321
  codecarto_guide: handleGuide,
1322
+ codecarto_broadside: handleBroadside,
1058
1323
  };
1059
1324
  export async function handleGuide(args) {
1060
1325
  const topics = await listGuideTopics();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecartographer-pi",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
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,13 +57,14 @@
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": {
55
64
  "@modelcontextprotocol/sdk": "^1.29.0"
56
65
  },
57
66
  "peerDependencies": {
58
- "@earendil-works/pi-coding-agent": "^0.80.10",
67
+ "@earendil-works/pi-coding-agent": ">=0.84.0",
59
68
  "@sinclair/typebox": "*"
60
69
  },
61
70
  "pi": {