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.
- package/.codecarto/GUIDE.md +16 -3
- package/.codecarto/README.md +3 -0
- package/.codecarto/broadside/SKILL.md +143 -0
- package/.codecarto/broadside/config.yaml +104 -0
- package/.codecarto/findings/architecture/SKILL.md +1 -0
- package/.codecarto/findings/broadside-scout/README.md +20 -0
- package/.codecarto/findings/broadside-scout/SKILL.md +101 -0
- package/.codecarto/findings/contracts/SKILL.md +1 -0
- package/.codecarto/findings/defect-scan/SKILL.md +15 -1
- package/.codecarto/findings/defect-scan/passes/01-logic-and-correctness.md +1 -1
- package/.codecarto/findings/defect-scan/passes/02-error-handling.md +1 -1
- package/.codecarto/findings/defect-scan/passes/03-concurrency-and-resources.md +1 -1
- package/.codecarto/findings/defect-scan/passes/04-security-and-trust.md +1 -1
- package/.codecarto/findings/defect-scan/passes/05-api-contract-violations.md +2 -1
- package/.codecarto/findings/defect-scan/passes/06-config-and-environment.md +1 -1
- package/.codecarto/findings/defect-scan-mechanical/SKILL.md +3 -2
- package/.codecarto/findings/defect-scan-semantic/SKILL.md +2 -0
- package/.codecarto/findings/porting/SKILL.md +2 -1
- package/.codecarto/findings/protocols/SKILL.md +1 -0
- package/.codecarto/findings/reimplementation-spec/SKILL.md +1 -1
- package/.codecarto/skills/spec-delta-application/SKILL.md +3 -1
- package/.codecarto/templates/architecture-map.md +1 -1
- package/.codecarto/templates/backlog-project.md +51 -0
- package/.codecarto/templates/broadside-scout-brief.md +97 -0
- package/.codecarto/templates/defect-report.md +23 -0
- package/.codecarto/templates/mechanical-defects.md +22 -0
- package/.codecarto/templates/reverse-engineering-bundle.md +2 -2
- package/.codecarto/templates/semantic-defects.md +26 -0
- package/.codecarto/{THREAD_LOG.md → templates/thread-log.md} +2 -5
- package/.codecarto/workflow/VALIDATE.md +1 -1
- package/.codecarto/workflow/pipeline-defect-scan.yaml +2 -0
- package/.codecarto/workflow/pipeline-full-with-audit.yaml +3 -1
- package/.codecarto/workflow/pipeline-full-with-deep-audit.yaml +5 -1
- package/.codecarto/workflow/pipeline-scout-first.yaml +275 -0
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/README.md +51 -6
- package/agent-skill/codecartographer/SKILL.md +3 -1
- package/agent-skill/codecartographer/references/broadside.md +115 -0
- package/agent-skill/codecartographer/references/deep-audit-synthesis.md +4 -1
- package/agent-skill/codecartographer/references/library.md +2 -2
- package/agent-skill/codecartographer/references/orchestration.md +1 -1
- package/agent-skill/codecartographer/references/pipeline-selection.md +14 -0
- package/dist/core/amendment.js +2 -2
- package/dist/core/broadside.d.ts +421 -0
- package/dist/core/broadside.js +2349 -0
- package/dist/core/completion.d.ts +5 -0
- package/dist/core/completion.js +38 -6
- package/dist/core/dashboard.js +5 -3
- package/dist/core/findings.d.ts +59 -0
- package/dist/core/findings.js +145 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/library.d.ts +88 -1
- package/dist/core/library.js +260 -7
- package/dist/core/orchestrator-config.js +5 -2
- package/dist/core/pipeline.js +16 -0
- package/dist/core/prompts.js +1 -1
- package/dist/core/status.js +23 -7
- package/dist/core/types.d.ts +6 -0
- package/dist/core/utils.d.ts +14 -0
- package/dist/core/utils.js +37 -1
- package/dist/core/workspace.d.ts +17 -0
- package/dist/core/workspace.js +79 -20
- package/dist/core/yaml.js +19 -4
- package/dist/extensions/codecarto/agent-runner.js +6 -0
- package/dist/extensions/codecarto/auto-runner.d.ts +2 -0
- package/dist/extensions/codecarto/broadside-flags.d.ts +26 -0
- package/dist/extensions/codecarto/broadside-flags.js +129 -0
- package/dist/extensions/codecarto/dashboard-narrator.js +1 -1
- package/dist/extensions/codecarto/index.js +270 -18
- package/dist/extensions/codecarto/phase-compaction.js +4 -0
- package/dist/mcp-server/server.d.ts +22 -0
- package/dist/mcp-server/server.js +282 -17
- package/package.json +11 -2
- package/.codecarto/BACKLOG.md +0 -184
- package/.codecarto/CHANGELOG-2026-05-02-feedback-pass.md +0 -118
- package/.codecarto/closeouts/2026-05-02-framework-feedback-pass.md +0 -111
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
// CodeCartographer MCP server. Exposes the framework
|
|
2
|
-
//
|
|
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 {
|
|
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
|
|
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
|
-
}, {
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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: {
|
|
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.
|
|
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": "
|
|
67
|
+
"@earendil-works/pi-coding-agent": ">=0.84.0",
|
|
59
68
|
"@sinclair/typebox": "*"
|
|
60
69
|
},
|
|
61
70
|
"pi": {
|