codecartographer-pi 0.22.3 → 0.24.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.
- package/.codecarto/broadside/SKILL.md +44 -1
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/README.md +5 -2
- package/agent-skill/codecartographer/references/broadside.md +5 -1
- package/dist/core/broadside-verify.d.ts +95 -0
- package/dist/core/broadside-verify.js +433 -0
- package/dist/core/broadside.d.ts +63 -0
- package/dist/core/broadside.js +79 -23
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/extensions/codecarto/broadside-flags.d.ts +4 -2
- package/dist/extensions/codecarto/broadside-flags.js +24 -6
- package/dist/extensions/codecarto/index.js +36 -4
- package/dist/mcp-server/server.d.ts +2 -1
- package/dist/mcp-server/server.js +45 -9
- package/package.json +1 -1
package/dist/core/broadside.js
CHANGED
|
@@ -698,6 +698,7 @@ const LENSES = {
|
|
|
698
698
|
"**/*handler*",
|
|
699
699
|
"**/*endpoint*",
|
|
700
700
|
],
|
|
701
|
+
fallbackGlobsFor: (info) => [info.sourceGlob],
|
|
701
702
|
systemPrompt: () => "You are a senior API auditor. Given source files from an HTTP server, " +
|
|
702
703
|
"extract every HTTP endpoint (method, path, handler function, auth requirement) " +
|
|
703
704
|
"and every key request/response data type. Return a JSON object following the " +
|
|
@@ -718,6 +719,7 @@ const LENSES = {
|
|
|
718
719
|
globsFor: (info) => info.language === "go"
|
|
719
720
|
? ["server/**/*.go", "server/*.go", "**/auth*.go", "**/middleware/**/*.go", "SECURITY.md"]
|
|
720
721
|
: ["server/**", "**/auth*", "**/middleware/**", "SECURITY.md"],
|
|
722
|
+
fallbackGlobsFor: (info) => [info.sourceGlob],
|
|
721
723
|
systemPrompt: () => "You are a security engineer performing a first-pass review of a codebase. " +
|
|
722
724
|
"Given source files, identify potential security issues — focusing on " +
|
|
723
725
|
"authentication, authorization, input validation, TLS, secrets handling, " +
|
|
@@ -918,7 +920,7 @@ const SOURCE_SPECS = {
|
|
|
918
920
|
* file list with uncommitted contents and never saw an untracked file (#248).
|
|
919
921
|
* A target that is not a git repository gets a bounded walk.
|
|
920
922
|
*/
|
|
921
|
-
async function listRepoFiles(targetDir) {
|
|
923
|
+
export async function listRepoFiles(targetDir) {
|
|
922
924
|
try {
|
|
923
925
|
const listed = await execFileAsync("git", ["-C", targetDir, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { maxBuffer: 64 * 1024 * 1024, timeout: GIT_TIMEOUT_MS });
|
|
924
926
|
const deleted = await execFileAsync("git", ["-C", targetDir, "ls-files", "-z", "--deleted"], {
|
|
@@ -1198,7 +1200,7 @@ function matchesAnyGlob(path, globs) {
|
|
|
1198
1200
|
}
|
|
1199
1201
|
return false;
|
|
1200
1202
|
}
|
|
1201
|
-
function isSlurpable(relPath) {
|
|
1203
|
+
export function isSlurpable(relPath) {
|
|
1202
1204
|
// A credential store is never a lens input, whatever its globs say (#252).
|
|
1203
1205
|
if (isSecretFile(relPath))
|
|
1204
1206
|
return false;
|
|
@@ -1237,8 +1239,7 @@ function resolveSliceMode(lens, files, totalChars) {
|
|
|
1237
1239
|
return lens.sliceBy;
|
|
1238
1240
|
return totalChars > lens.maxChars ? "directory" : "none";
|
|
1239
1241
|
}
|
|
1240
|
-
function
|
|
1241
|
-
const globs = lens.globsFor(info).filter(Boolean);
|
|
1242
|
+
function collectFilesMatching(allFiles, lens, globs) {
|
|
1242
1243
|
if (globs.length === 0)
|
|
1243
1244
|
return [];
|
|
1244
1245
|
const out = [];
|
|
@@ -1253,6 +1254,30 @@ function collectLensFiles(allFiles, lens, info) {
|
|
|
1253
1254
|
}
|
|
1254
1255
|
return out;
|
|
1255
1256
|
}
|
|
1257
|
+
/**
|
|
1258
|
+
* The files a lens will read: its targeted globs, or — when those match
|
|
1259
|
+
* nothing and the lens declares a fallback — the fallback globs, with a
|
|
1260
|
+
* sentence saying so (#319). The sentence travels to the estimate, the
|
|
1261
|
+
* batch entry, and the prompt, so a fallback scan is never a silent one.
|
|
1262
|
+
*/
|
|
1263
|
+
export function selectLensFiles(allFiles, lens, info) {
|
|
1264
|
+
const globs = lens.globsFor(info).filter(Boolean);
|
|
1265
|
+
const targeted = collectFilesMatching(allFiles, lens, globs);
|
|
1266
|
+
if (targeted.length > 0 || globs.length === 0 || !lens.fallbackGlobsFor)
|
|
1267
|
+
return { files: targeted };
|
|
1268
|
+
const fallbackGlobs = lens.fallbackGlobsFor(info).filter(Boolean);
|
|
1269
|
+
const files = collectFilesMatching(allFiles, lens, fallbackGlobs);
|
|
1270
|
+
if (files.length === 0)
|
|
1271
|
+
return { files };
|
|
1272
|
+
return {
|
|
1273
|
+
files,
|
|
1274
|
+
fallback: `no files matched ${globs.join(", ")}${lens.skipTestFiles ? " (test files excluded)" : ""}; ` +
|
|
1275
|
+
`scanned all ${info.language} sources (${fallbackGlobs.join(", ")}) instead`,
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
function collectLensFiles(allFiles, lens, info) {
|
|
1279
|
+
return selectLensFiles(allFiles, lens, info).files;
|
|
1280
|
+
}
|
|
1256
1281
|
async function slurpFileList(targetDir, files, maxChars, redact = true) {
|
|
1257
1282
|
const slices = [];
|
|
1258
1283
|
let currentModule = "";
|
|
@@ -1329,16 +1354,18 @@ export async function gatherSlices(targetDir, lens, info, opts = {}) {
|
|
|
1329
1354
|
return [{ moduleName: "root", content: "", fileCount: 0, chars: 0, files: [] }];
|
|
1330
1355
|
}
|
|
1331
1356
|
const { files: allFiles } = await listRepoFiles(targetDir);
|
|
1332
|
-
const files =
|
|
1357
|
+
const { files, fallback } = selectLensFiles(allFiles, lens, info);
|
|
1333
1358
|
const totalChars = await sumFileSizes(targetDir, files);
|
|
1334
1359
|
const mode = resolveSliceMode(lens, files, totalChars);
|
|
1335
|
-
|
|
1360
|
+
const slices = mode === "none"
|
|
1336
1361
|
// Whole-repo slice: one module named after the repo, so a small
|
|
1337
1362
|
// repo produces a single request instead of one per directory.
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1363
|
+
? await slurpFileList(targetDir, files.map((f) => ({ ...f, moduleName: info.name })), lens.maxChars, redact)
|
|
1364
|
+
: await slurpFileList(targetDir, files, lens.maxChars, redact);
|
|
1365
|
+
if (fallback)
|
|
1366
|
+
for (const slice of slices)
|
|
1367
|
+
slice.fallback = fallback;
|
|
1368
|
+
return slices;
|
|
1342
1369
|
}
|
|
1343
1370
|
async function sumFileSizes(targetDir, files) {
|
|
1344
1371
|
let total = 0;
|
|
@@ -1362,7 +1389,17 @@ export function buildBatchRequest(lens, info, slice, index, sliceCount, model =
|
|
|
1362
1389
|
model,
|
|
1363
1390
|
messages: [
|
|
1364
1391
|
{ role: "system", content: lens.systemPrompt(info) },
|
|
1365
|
-
{
|
|
1392
|
+
{
|
|
1393
|
+
role: "user",
|
|
1394
|
+
content:
|
|
1395
|
+
// A fallback scan is not "server source files": say what it is,
|
|
1396
|
+
// so the model judges the trust boundary wherever it appears
|
|
1397
|
+
// and does not report the missing server/ as a finding (#319).
|
|
1398
|
+
(slice.fallback
|
|
1399
|
+
? `NOTE: this repository has no files under the paths this lens usually reads (${slice.fallback}). ` +
|
|
1400
|
+
"What follows is every source file it has; locate the trust boundary and the request-handling code wherever they live.\n\n"
|
|
1401
|
+
: "") + lens.userPrompt(info, slice.content, slice.moduleName),
|
|
1402
|
+
},
|
|
1366
1403
|
],
|
|
1367
1404
|
response_format: { type: "json_schema", json_schema: SCHEMAS[lens.schemaName] },
|
|
1368
1405
|
max_tokens: maxTokensOverride ?? lens.maxTokens,
|
|
@@ -1587,6 +1624,10 @@ export async function persistBroadsideRunMerging(broadsideDir, run) {
|
|
|
1587
1624
|
run.triage = onDisk.triage;
|
|
1588
1625
|
if (retryEntryRank(onDisk.retry) > retryEntryRank(run.retry))
|
|
1589
1626
|
run.retry = onDisk.retry;
|
|
1627
|
+
// A verification pass another process recorded is never dropped by
|
|
1628
|
+
// a collect that never knew about it; a newer pass replaces an older.
|
|
1629
|
+
if (onDisk.verify && (!run.verify || onDisk.verify.at > run.verify.at))
|
|
1630
|
+
run.verify = onDisk.verify;
|
|
1590
1631
|
for (const [lensId, theirs] of Object.entries(onDisk.batches)) {
|
|
1591
1632
|
if (theirs && batchEntryRank(theirs) > batchEntryRank(run.batches[lensId]))
|
|
1592
1633
|
run.batches[lensId] = theirs;
|
|
@@ -2320,11 +2361,14 @@ export async function runBroadsideSubmit(cwd, apiKey, opts = {}) {
|
|
|
2320
2361
|
slicesByLens.set(lensId, slices);
|
|
2321
2362
|
if (slices.length === 0) {
|
|
2322
2363
|
const globs = lens.globsFor(info).filter(Boolean);
|
|
2364
|
+
const fallbackGlobs = lens.fallbackGlobsFor?.(info).filter(Boolean) ?? [];
|
|
2323
2365
|
skipReasons.set(lensId, globs.length === 0
|
|
2324
2366
|
? "the lens has no file patterns for this language"
|
|
2325
2367
|
: matchedBeforeIncremental > 0
|
|
2326
2368
|
? "incremental: none of this lens's files changed since the previous run"
|
|
2327
|
-
: `no files matched ${globs.join(", ")}
|
|
2369
|
+
: `no files matched ${globs.join(", ")}` +
|
|
2370
|
+
(fallbackGlobs.length > 0 ? ` or the fallback ${fallbackGlobs.join(", ")}` : "") +
|
|
2371
|
+
(lens.skipTestFiles ? " (test files excluded)" : ""));
|
|
2328
2372
|
}
|
|
2329
2373
|
const lensModel = modelForLens(lensId);
|
|
2330
2374
|
const { pricing: lensPricing, outputCap: lensOutputCap } = resolved.get(lensModel);
|
|
@@ -2347,15 +2391,19 @@ export async function runBroadsideSubmit(cwd, apiKey, opts = {}) {
|
|
|
2347
2391
|
const approved = await opts.confirm({
|
|
2348
2392
|
model,
|
|
2349
2393
|
pricing,
|
|
2350
|
-
lenses: perLensEstimate.map(({ lens, cost, maxTokens, lensModel, lensPricing }) =>
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2394
|
+
lenses: perLensEstimate.map(({ lens, cost, maxTokens, lensModel, lensPricing }) => {
|
|
2395
|
+
const fallback = (slicesByLens.get(lens.id) ?? []).find((slice) => slice.fallback)?.fallback;
|
|
2396
|
+
return {
|
|
2397
|
+
lensId: lens.id,
|
|
2398
|
+
name: lens.name,
|
|
2399
|
+
slices: (slicesByLens.get(lens.id) ?? []).length,
|
|
2400
|
+
maxTokens,
|
|
2401
|
+
cost,
|
|
2402
|
+
model: lensModel,
|
|
2403
|
+
pricing: lensPricing,
|
|
2404
|
+
...(fallback && { fallback }),
|
|
2405
|
+
};
|
|
2406
|
+
}),
|
|
2359
2407
|
mixedModels: perLensEstimate.some(({ lensModel }) => lensModel !== model),
|
|
2360
2408
|
totalCost: estimatedTotalCost,
|
|
2361
2409
|
inputTokens: estimatedInputTokens,
|
|
@@ -2419,12 +2467,15 @@ export async function runBroadsideSubmit(cwd, apiKey, opts = {}) {
|
|
|
2419
2467
|
const requests = slices.map((sl, i) => buildBatchRequest(lens, info, sl, i, slices.length, lensModel, maxTokens, config.reasoning ?? undefined));
|
|
2420
2468
|
for (const request of requests)
|
|
2421
2469
|
requestsByCustomId[request.custom_id] = request;
|
|
2470
|
+
const fallback = slices.find((slice) => slice.fallback)?.fallback;
|
|
2422
2471
|
const entry = {
|
|
2423
2472
|
batchId: "",
|
|
2424
2473
|
requests: requests.length,
|
|
2425
2474
|
status: "submitting",
|
|
2426
2475
|
submittedAt: new Date().toISOString(),
|
|
2427
2476
|
estimatedCost: priced.cost,
|
|
2477
|
+
// A scan of the fallback scope is recorded as such (#319).
|
|
2478
|
+
...(fallback && { fallback }),
|
|
2428
2479
|
// Recorded per lens so collect's truncation retry re-submits against
|
|
2429
2480
|
// the model and ceiling this lens actually used, not the run default.
|
|
2430
2481
|
...(lensModel !== model && { model: lensModel }),
|
|
@@ -3256,7 +3307,9 @@ export function estimateSubmitText(result, lenses) {
|
|
|
3256
3307
|
? ` — ${explainBatchError(entry.error)}`
|
|
3257
3308
|
: !entry.batchId && entry.reason
|
|
3258
3309
|
? ` — ${entry.reason}`
|
|
3259
|
-
:
|
|
3310
|
+
: entry.fallback
|
|
3311
|
+
? ` — ${entry.fallback}`
|
|
3312
|
+
: "";
|
|
3260
3313
|
lines.push(` ${lens.name}: ${status} (${entry.requests} request(s), ~$${entry.estimatedCost.toFixed(4)})${override}${reason}`);
|
|
3261
3314
|
}
|
|
3262
3315
|
if (result.repo) {
|
|
@@ -3492,10 +3545,13 @@ export function statusText(state) {
|
|
|
3492
3545
|
if (!entry)
|
|
3493
3546
|
continue;
|
|
3494
3547
|
lines.push(` ${lensId}: ${entry.status}${entry.batchId ? ` (${entry.batchId})` : ""}${entry.cost !== undefined ? `, $${entry.cost.toFixed(6)}` : ""}` +
|
|
3495
|
-
(entry.status === "skipped" && entry.reason ? ` — ${entry.reason}` : ""));
|
|
3548
|
+
(entry.status === "skipped" && entry.reason ? ` — ${entry.reason}` : entry.fallback ? ` — ${entry.fallback}` : ""));
|
|
3496
3549
|
}
|
|
3497
3550
|
lines.push(` synthesis: ${run.synthesis.status}`);
|
|
3498
3551
|
lines.push(` triage: ${run.triage?.status ?? "pending"}`);
|
|
3552
|
+
if (run.verify) {
|
|
3553
|
+
lines.push(` verify: ${run.verify.status} — ${run.verify.confirmed} confirmed of ${run.verify.verified} read on ${run.verify.model}, $${run.verify.cost.toFixed(4)}`);
|
|
3554
|
+
}
|
|
3499
3555
|
if (run.totalCost !== undefined)
|
|
3500
3556
|
lines.push(` total cost: $${run.totalCost.toFixed(6)}`);
|
|
3501
3557
|
}
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type BroadsideLensId } from "../../core/index.ts";
|
|
2
|
-
export type BroadsideAction = "submit" | "collect" | "status" | "models";
|
|
2
|
+
export type BroadsideAction = "submit" | "collect" | "status" | "models" | "verify";
|
|
3
3
|
export interface BroadsideFlags {
|
|
4
4
|
action: BroadsideAction;
|
|
5
5
|
/** Empty means "the repository's default lens set". */
|
|
@@ -22,11 +22,13 @@ export interface BroadsideFlags {
|
|
|
22
22
|
model?: string;
|
|
23
23
|
/** For submit: per-lens model overrides, layered over config.yaml's (#141). */
|
|
24
24
|
lensModels?: Partial<Record<BroadsideLensId, string>>;
|
|
25
|
+
/** For verify: how many findings to read (#143). */
|
|
26
|
+
top?: number;
|
|
25
27
|
benchmarks: boolean;
|
|
26
28
|
unknown: string[];
|
|
27
29
|
/** Set on an invalid combination. The caller surfaces it as an error. */
|
|
28
30
|
error?: string;
|
|
29
31
|
}
|
|
30
32
|
/** Every token the completer offers, in the order it offers them. */
|
|
31
|
-
export declare const KNOWN_BROADSIDE_TOKENS: readonly ["submit", "collect", "status", "models", "architecture", "api", "security", "defect", "conventions", "porting", "--incremental", "--no-incremental", "--max-cost=", "--wait=", "--run=", "--model=", "--lens-model=", "--no-synthesis", "--no-triage", "--no-retry-truncated", "--benchmarks"];
|
|
33
|
+
export declare const KNOWN_BROADSIDE_TOKENS: readonly ["submit", "collect", "status", "models", "verify", "architecture", "api", "security", "defect", "conventions", "porting", "--incremental", "--no-incremental", "--max-cost=", "--wait=", "--run=", "--model=", "--lens-model=", "--top=", "--no-synthesis", "--no-triage", "--no-retry-truncated", "--benchmarks"];
|
|
32
34
|
export declare function parseBroadsideFlags(args: string): BroadsideFlags;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// /codecarto-broadside collect --wait=900
|
|
7
7
|
// /codecarto-broadside status
|
|
8
8
|
// /codecarto-broadside models --benchmarks
|
|
9
|
+
// /codecarto-broadside verify --top=10 → read the top findings against the source
|
|
9
10
|
//
|
|
10
11
|
// Flags mirror the codecarto_broadside tool parameters, with the negative
|
|
11
12
|
// forms spelled out because a slash command has no place to pass `false`:
|
|
@@ -14,8 +15,9 @@
|
|
|
14
15
|
// --max-cost=N --no-retry-truncated
|
|
15
16
|
// --wait=SECONDS --benchmarks (models only)
|
|
16
17
|
// --run=ID (collect only: an older run, as listed by status)
|
|
17
|
-
// --model=ID (submit
|
|
18
|
+
// --model=ID (submit: the run's batch model, as listed by models; verify: the sync model to read with)
|
|
18
19
|
// --lens-model=LENS:ID (submit only, repeatable: one lens on its own model)
|
|
20
|
+
// --top=N (verify only: how many findings to read, most severe first)
|
|
19
21
|
//
|
|
20
22
|
// A model id itself contains a colon (`vendor/name:batch`), so --lens-model
|
|
21
23
|
// splits on the first colon only: `security:deepseek/deepseek-v4-pro:batch`.
|
|
@@ -28,13 +30,14 @@
|
|
|
28
30
|
// The parser never throws. index.ts decides how to surface unknown tokens and
|
|
29
31
|
// invalid combinations, matching parseNextFlags.
|
|
30
32
|
import { BROADSIDE_LENS_IDS } from "../../core/index.js";
|
|
31
|
-
const ACTIONS = new Set(["submit", "collect", "status", "models"]);
|
|
33
|
+
const ACTIONS = new Set(["submit", "collect", "status", "models", "verify"]);
|
|
32
34
|
/** Every token the completer offers, in the order it offers them. */
|
|
33
35
|
export const KNOWN_BROADSIDE_TOKENS = [
|
|
34
36
|
"submit",
|
|
35
37
|
"collect",
|
|
36
38
|
"status",
|
|
37
39
|
"models",
|
|
40
|
+
"verify",
|
|
38
41
|
...BROADSIDE_LENS_IDS,
|
|
39
42
|
"--incremental",
|
|
40
43
|
"--no-incremental",
|
|
@@ -43,6 +46,7 @@ export const KNOWN_BROADSIDE_TOKENS = [
|
|
|
43
46
|
"--run=",
|
|
44
47
|
"--model=",
|
|
45
48
|
"--lens-model=",
|
|
49
|
+
"--top=",
|
|
46
50
|
"--no-synthesis",
|
|
47
51
|
"--no-triage",
|
|
48
52
|
"--no-retry-truncated",
|
|
@@ -122,6 +126,14 @@ export function parseBroadsideFlags(args) {
|
|
|
122
126
|
result.runId = value || undefined;
|
|
123
127
|
continue;
|
|
124
128
|
}
|
|
129
|
+
if (token.startsWith("--top=")) {
|
|
130
|
+
const value = parseNumeric(token, "--top", result);
|
|
131
|
+
if (value !== undefined && (!Number.isInteger(value) || value < 1))
|
|
132
|
+
result.error ??= `--top needs a positive whole number (got "${token.slice("--top=".length)}").`;
|
|
133
|
+
else if (value !== undefined)
|
|
134
|
+
result.top = value;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
125
137
|
if (token.startsWith("--model=")) {
|
|
126
138
|
const value = token.slice("--model=".length).trim();
|
|
127
139
|
// An empty value is a mistyped selection, not "use the default":
|
|
@@ -167,11 +179,17 @@ export function parseBroadsideFlags(args) {
|
|
|
167
179
|
if (result.action === "status" && result.waitSeconds !== undefined) {
|
|
168
180
|
result.error ??= "--wait is only meaningful for submit and collect; status reads recorded state.";
|
|
169
181
|
}
|
|
170
|
-
if (result.runId !== undefined && result.action !== "collect") {
|
|
171
|
-
result.error ??= `--run is only meaningful for collect (got action "${result.action}").`;
|
|
182
|
+
if (result.runId !== undefined && result.action !== "collect" && result.action !== "verify") {
|
|
183
|
+
result.error ??= `--run is only meaningful for collect and verify (got action "${result.action}").`;
|
|
184
|
+
}
|
|
185
|
+
if (result.model !== undefined && result.action !== "submit" && result.action !== "verify") {
|
|
186
|
+
result.error ??= `--model is only meaningful for submit and verify (got action "${result.action}").`;
|
|
187
|
+
}
|
|
188
|
+
if (result.top !== undefined && result.action !== "verify") {
|
|
189
|
+
result.error ??= `--top is only meaningful for verify (got action "${result.action}").`;
|
|
172
190
|
}
|
|
173
|
-
if (result.
|
|
174
|
-
result.error ??=
|
|
191
|
+
if (result.action === "verify" && result.waitSeconds !== undefined) {
|
|
192
|
+
result.error ??= "--wait is only meaningful for submit and collect; verify runs to completion.";
|
|
175
193
|
}
|
|
176
194
|
if (result.lensModels !== undefined && result.action !== "submit") {
|
|
177
195
|
result.error ??= `--lens-model is only meaningful for submit (got action "${result.action}").`;
|
|
@@ -10,7 +10,7 @@ import { completeLastToken } from "./completions.js";
|
|
|
10
10
|
import { buildPiGuideMessage } from "./guide-framing.js";
|
|
11
11
|
import { isCtxLive, notifyCtx } from "./notify.js";
|
|
12
12
|
import { phaseCompactionExtension } from "./phase-compaction.js";
|
|
13
|
-
import { applyAmendment, buildPhasePrompt, buildSkillPrompt, buildValidationSummary, backupWorkspaceState, canonicalPath, copyPackagedWorkspace, computePerPhaseTotals, computeTotals, ConfidentialityMismatchError, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, describeDanglingCarryForward, describeMissingCompletedOutputs, describeStuckPipeline, expandTilde, getPipelineLabel, getWorkspaceState, isWithinPath, resolveExistingPrefix, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, BroadsideConfigError, defaultBroadsideConfig, BroadsideCancelledError, broadsideDirFor, collectResultText, estimateSubmitText, getLens, listAmendmentNames, listBatchModels, listGuideTopics, listScaffoldRefreshFiles, listMissingCompletedOutputs, listSkillNames, resolvePipelineOutcome, resolveSkillName, loadAmendmentFile, loadBroadsideConfig, modelsText, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, statusText, describeConfigProblems, describeIncrementalFallback, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, readBroadsideSkill, readGuide, refreshScaffold, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, resolvePublishSourceRepo, SourceRepoMismatchError, runPhasePreflight, SCAFFOLD_REFRESH_PROTECTED, seedOrchestratorFiles, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, writeDashboard, } from "../../core/index.js";
|
|
13
|
+
import { applyAmendment, buildPhasePrompt, buildSkillPrompt, buildValidationSummary, backupWorkspaceState, canonicalPath, copyPackagedWorkspace, computePerPhaseTotals, computeTotals, ConfidentialityMismatchError, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, describeDanglingCarryForward, describeMissingCompletedOutputs, describeStuckPipeline, expandTilde, getPipelineLabel, getWorkspaceState, isWithinPath, resolveExistingPrefix, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, BroadsideConfigError, defaultBroadsideConfig, BroadsideCancelledError, broadsideDirFor, collectResultText, estimateSubmitText, getLens, listAmendmentNames, listBatchModels, listGuideTopics, listScaffoldRefreshFiles, listMissingCompletedOutputs, listSkillNames, resolvePipelineOutcome, resolveSkillName, loadAmendmentFile, loadBroadsideConfig, modelsText, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, runBroadsideVerify, verifyResultText, statusText, describeConfigProblems, describeIncrementalFallback, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, readBroadsideSkill, readGuide, refreshScaffold, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, resolvePublishSourceRepo, SourceRepoMismatchError, runPhasePreflight, SCAFFOLD_REFRESH_PROTECTED, seedOrchestratorFiles, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, writeDashboard, } from "../../core/index.js";
|
|
14
14
|
import { initLibrary } from "../../core/library.js";
|
|
15
15
|
import { resolveUserConfigPath } from "../../core/orchestrator-config.js";
|
|
16
16
|
const STATUS_WIDGET_ID = "codecarto-widget";
|
|
@@ -130,11 +130,14 @@ function describeBroadsideEstimate(estimate) {
|
|
|
130
130
|
`Rates: $${estimate.pricing.inputPerM.toFixed(4)}/M in · $${estimate.pricing.outputPerM.toFixed(4)}/M out`,
|
|
131
131
|
"",
|
|
132
132
|
"Per lens:",
|
|
133
|
-
...estimate.lenses.map(({ name, slices, cost, model }) => {
|
|
133
|
+
...estimate.lenses.map(({ name, slices, cost, model, fallback }) => {
|
|
134
134
|
// Naming the model only when it differs keeps the common case quiet
|
|
135
135
|
// and makes a mixed-model run impossible to approve without noticing.
|
|
136
136
|
const override = estimate.mixedModels && model !== estimate.model ? ` on ${model}` : "";
|
|
137
|
-
|
|
137
|
+
// A lens priced on its fallback scope says so here, where the
|
|
138
|
+
// spend is approved — every source file, not a server directory (#319).
|
|
139
|
+
const scope = fallback ? `\n ↳ ${fallback}` : "";
|
|
140
|
+
return ` ${name}: ${slices} slice${slices === 1 ? "" : "s"} — ~$${cost.toFixed(4)}${override}${scope}`;
|
|
138
141
|
}),
|
|
139
142
|
"",
|
|
140
143
|
`Estimated total: ~$${estimate.totalCost.toFixed(4)} ` +
|
|
@@ -992,7 +995,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
992
995
|
},
|
|
993
996
|
});
|
|
994
997
|
pi.registerCommand("codecarto-broadside", {
|
|
995
|
-
description: "Batch reconnaissance (Broad-Side): /codecarto-broadside [submit|collect|status|models] [lenses…] [--model=ID] [--lens-model=LENS:ID] [flags]",
|
|
998
|
+
description: "Batch reconnaissance (Broad-Side): /codecarto-broadside [submit|collect|status|models|verify] [lenses…] [--model=ID] [--lens-model=LENS:ID] [--top=N] [flags]",
|
|
996
999
|
// Completes the token under the cursor, so lens names and flags are
|
|
997
1000
|
// offered after the action too, and keeps everything typed before it.
|
|
998
1001
|
getArgumentCompletions: (prefix) => completeLastToken(prefix, KNOWN_BROADSIDE_TOKENS.map((value) => ({ value }))),
|
|
@@ -1178,6 +1181,35 @@ export default function codeCartographerExtension(pi) {
|
|
|
1178
1181
|
}
|
|
1179
1182
|
return;
|
|
1180
1183
|
}
|
|
1184
|
+
if (flags.action === "verify") {
|
|
1185
|
+
// The verification pass (#143): one sync call per finding with
|
|
1186
|
+
// read-only tools; the widget counts verdicts as they land.
|
|
1187
|
+
const tally = { done: 0 };
|
|
1188
|
+
renderProgress("Reading the top findings against the source…");
|
|
1189
|
+
try {
|
|
1190
|
+
const verified = await runBroadsideVerify(ctx.cwd, apiKey, {
|
|
1191
|
+
...(flags.runId && { runId: flags.runId }),
|
|
1192
|
+
...(flags.top !== undefined && { top: flags.top }),
|
|
1193
|
+
...(flags.model && { model: flags.model }),
|
|
1194
|
+
maxCost: flags.maxCost ?? config.maxCost,
|
|
1195
|
+
signal: ctx.signal,
|
|
1196
|
+
onProgress: (finding) => {
|
|
1197
|
+
tally.done += 1;
|
|
1198
|
+
progress.set(`#${finding.index}`, `${finding.verdict} — ${finding.title}`);
|
|
1199
|
+
renderProgress(`Verifying findings… ${tally.done} read`);
|
|
1200
|
+
},
|
|
1201
|
+
});
|
|
1202
|
+
const lines = verifyResultText(verified).split("\n");
|
|
1203
|
+
const confirmed = verified.findings.filter((f) => f.verdict === "confirmed").length;
|
|
1204
|
+
finish(lines, `Broad-Side verify: ${confirmed} confirmed of ${verified.findings.length} read`, verified.status === "completed" ? "info" : "warning");
|
|
1205
|
+
}
|
|
1206
|
+
catch (error) {
|
|
1207
|
+
if (ctx.hasUI)
|
|
1208
|
+
ctx.ui.setWidget(BROADSIDE_WIDGET_ID, undefined);
|
|
1209
|
+
notifyCtx(ctx, `Broad-Side verify failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1210
|
+
}
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1181
1213
|
// action === "collect"
|
|
1182
1214
|
renderProgress("Polling batches…");
|
|
1183
1215
|
try {
|
|
@@ -233,7 +233,7 @@ export declare function handleAmend(args: {
|
|
|
233
233
|
}>;
|
|
234
234
|
export declare function handleBroadside(args: {
|
|
235
235
|
cwd: string;
|
|
236
|
-
action: "submit" | "collect" | "status" | "models";
|
|
236
|
+
action: "submit" | "collect" | "status" | "models" | "verify";
|
|
237
237
|
lenses?: string[];
|
|
238
238
|
api_key?: string;
|
|
239
239
|
wait_seconds?: number;
|
|
@@ -247,6 +247,7 @@ export declare function handleBroadside(args: {
|
|
|
247
247
|
incremental?: boolean;
|
|
248
248
|
model?: string;
|
|
249
249
|
lens_models?: Record<string, string>;
|
|
250
|
+
top?: number;
|
|
250
251
|
}): Promise<{
|
|
251
252
|
content: {
|
|
252
253
|
type: "text";
|
|
@@ -17,7 +17,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
17
17
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
18
18
|
import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
19
19
|
import { basename, isAbsolute, join } from "node:path";
|
|
20
|
-
import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, BROADSIDE_DIR, broadsideDirFor, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, BroadsideConfigError, defaultBroadsideConfig, backupWorkspaceState, canonicalPath, copyPackagedWorkspace, collectResultText, completeValidatedPhase, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, describeScaffoldStaleness, detectProvenanceConflicts, estimateSubmitText, getLens, describeDanglingCarryForward, describeMissingCompletedOutputs, describeStuckPipeline, getPipelineLabel, getWorkspaceState, isValidSlug, isWithinPathResolved, listBatchModels, listEntries, listGuideTopics, loadBroadsideConfig, modelsText, readGuide, listMissingCompletedOutputs, listSkillNames, resolvePipelineOutcome, resolveSkillName, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, PACKAGE_VERSION, packagedWorkspaceDir, pathExists, PhasePreflightError, previewPublishVersion, publishEntry, reindex as libraryReindex, refreshScaffold, resolvePhase, resolvePipelineChoice, readBroadsideSkill, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, seedOrchestratorFiles, statusLineWriter, statusText, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, writeDashboard, } from "../core/index.js";
|
|
20
|
+
import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, BROADSIDE_DIR, broadsideDirFor, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, BroadsideConfigError, defaultBroadsideConfig, backupWorkspaceState, canonicalPath, copyPackagedWorkspace, collectResultText, completeValidatedPhase, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, describeScaffoldStaleness, detectProvenanceConflicts, estimateSubmitText, getLens, describeDanglingCarryForward, describeMissingCompletedOutputs, describeStuckPipeline, getPipelineLabel, getWorkspaceState, isValidSlug, isWithinPathResolved, listBatchModels, listEntries, listGuideTopics, loadBroadsideConfig, modelsText, readGuide, listMissingCompletedOutputs, listSkillNames, resolvePipelineOutcome, resolveSkillName, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, PACKAGE_VERSION, packagedWorkspaceDir, pathExists, PhasePreflightError, previewPublishVersion, publishEntry, reindex as libraryReindex, refreshScaffold, resolvePhase, resolvePipelineChoice, readBroadsideSkill, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, runBroadsideVerify, verifyResultText, seedOrchestratorFiles, statusLineWriter, statusText, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, writeDashboard, } from "../core/index.js";
|
|
21
21
|
import { applyAmendment } from "../core/amendment.js";
|
|
22
22
|
import { appendUsageRun } from "../core/usage.js";
|
|
23
23
|
import { initLibrary } from "../core/library.js";
|
|
@@ -1037,8 +1037,8 @@ function resolveBroadsideApiKey(explicit, config) {
|
|
|
1037
1037
|
export async function handleBroadside(args) {
|
|
1038
1038
|
const cwd = await validateCwd(args.cwd);
|
|
1039
1039
|
const action = args.action ?? "submit";
|
|
1040
|
-
if (!["submit", "collect", "status", "models"].includes(action)) {
|
|
1041
|
-
throw new McpError(ErrorCode.InvalidParams, `Unknown action: ${action}. Valid actions: submit, collect, status, models.`);
|
|
1040
|
+
if (!["submit", "collect", "status", "models", "verify"].includes(action)) {
|
|
1041
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown action: ${action}. Valid actions: submit, collect, status, models, verify.`);
|
|
1042
1042
|
}
|
|
1043
1043
|
// A config.yaml that exists but cannot be read refuses every action that
|
|
1044
1044
|
// would act on it (#232); status only reads recorded runs, so it answers
|
|
@@ -1173,8 +1173,40 @@ export async function handleBroadside(args) {
|
|
|
1173
1173
|
maxCost: result.maxCost,
|
|
1174
1174
|
});
|
|
1175
1175
|
}
|
|
1176
|
-
// action === "collect"
|
|
1177
1176
|
const runId = typeof args.run_id === "string" && args.run_id.trim() ? args.run_id.trim() : undefined;
|
|
1177
|
+
if (action === "verify") {
|
|
1178
|
+
// The verification pass (#143): one sync call per finding with read-only
|
|
1179
|
+
// tools, most severe first. `max_cost` is a running cap here — a sync
|
|
1180
|
+
// call's cost is known only when it returns — so the pass stops before
|
|
1181
|
+
// the next finding once reached; absent, config.yaml's cap applies.
|
|
1182
|
+
if (args.top !== undefined && !(typeof args.top === "number" && Number.isInteger(args.top) && args.top >= 1)) {
|
|
1183
|
+
throw new McpError(ErrorCode.InvalidParams, "top must be a positive integer.");
|
|
1184
|
+
}
|
|
1185
|
+
if (args.model !== undefined && !(typeof args.model === "string" && args.model.trim())) {
|
|
1186
|
+
throw new McpError(ErrorCode.InvalidParams, "model must be a non-empty OpenRouter model id.");
|
|
1187
|
+
}
|
|
1188
|
+
const maxCost = typeof args.max_cost === "number" && args.max_cost >= 0 ? args.max_cost : config.maxCost;
|
|
1189
|
+
const verified = await runBroadsideVerify(cwd, apiKey, {
|
|
1190
|
+
...(runId && { runId }),
|
|
1191
|
+
...(args.top !== undefined && { top: args.top }),
|
|
1192
|
+
...(typeof args.model === "string" && args.model.trim() && { model: args.model.trim() }),
|
|
1193
|
+
maxCost,
|
|
1194
|
+
signal: serverLifetime?.signal,
|
|
1195
|
+
}).catch((error) => {
|
|
1196
|
+
throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
|
|
1197
|
+
});
|
|
1198
|
+
return textResult(verifyResultText(verified), {
|
|
1199
|
+
runId: verified.runId,
|
|
1200
|
+
outputDir: verified.outputDir,
|
|
1201
|
+
status: verified.status,
|
|
1202
|
+
model: verified.model,
|
|
1203
|
+
candidates: verified.candidates,
|
|
1204
|
+
totalCost: verified.totalCost,
|
|
1205
|
+
...(verified.stoppedByCost && { stoppedByCost: true }),
|
|
1206
|
+
findings: verified.findings,
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
// action === "collect"
|
|
1178
1210
|
const collect = await runBroadsideCollect(cwd, apiKey, {
|
|
1179
1211
|
waitMs,
|
|
1180
1212
|
includeSynthesis,
|
|
@@ -1502,8 +1534,8 @@ const TOOLS = [
|
|
|
1502
1534
|
cwd: { type: "string", description: "Absolute path to the target repository." },
|
|
1503
1535
|
action: {
|
|
1504
1536
|
type: "string",
|
|
1505
|
-
enum: ["submit", "collect", "status", "models"],
|
|
1506
|
-
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.",
|
|
1537
|
+
enum: ["submit", "collect", "status", "models", "verify"],
|
|
1538
|
+
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; verify reads a collected run's top defect and security findings against the repository with read-only tools (one sync-priced call each, about a cent on the default model) and writes verified.md/verified.json beside triage.md with a verdict per finding: confirmed (a reachable failure, with the trigger), not-a-defect, discarded, or unclear.",
|
|
1507
1539
|
},
|
|
1508
1540
|
lenses: {
|
|
1509
1541
|
type: "array",
|
|
@@ -1516,7 +1548,11 @@ const TOOLS = [
|
|
|
1516
1548
|
},
|
|
1517
1549
|
run_id: {
|
|
1518
1550
|
type: "string",
|
|
1519
|
-
description: "For collect: the run to
|
|
1551
|
+
description: "For collect and verify: the run to act on, as listed by the status action. Defaults to the most recent run; pass this to collect an older run that is still in flight after a newer submit.",
|
|
1552
|
+
},
|
|
1553
|
+
top: {
|
|
1554
|
+
type: "integer",
|
|
1555
|
+
description: "For verify: how many findings to read, most severe first (default 10). Each costs one sync call; max_cost caps the pass as a running total.",
|
|
1520
1556
|
},
|
|
1521
1557
|
wait_seconds: {
|
|
1522
1558
|
type: "number",
|
|
@@ -1536,7 +1572,7 @@ const TOOLS = [
|
|
|
1536
1572
|
},
|
|
1537
1573
|
max_cost: {
|
|
1538
1574
|
type: "number",
|
|
1539
|
-
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, whose default is $1.00; pass 0 for no limit.",
|
|
1575
|
+
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. For verify it is a running cap: the pass stops before the next finding once the calls so far have reached it. Falls back to max_cost in .codecarto/broadside/config.yaml, whose default is $1.00; pass 0 for no limit.",
|
|
1540
1576
|
},
|
|
1541
1577
|
force: {
|
|
1542
1578
|
type: "boolean",
|
|
@@ -1552,7 +1588,7 @@ const TOOLS = [
|
|
|
1552
1588
|
},
|
|
1553
1589
|
model: {
|
|
1554
1590
|
type: "string",
|
|
1555
|
-
description: "For submit: the OpenRouter batch model for this run (an id ending in :batch, as listed by action 'models'). Falls back to model in .codecarto/broadside/config.yaml, then the shipped default. Pre-flighted like the configured model: priced from the live catalog, refused without structured-output support, clamped to its completion ceiling. The models listing is advisory — some catalog ids have no batch endpoint and are refused at submit, at no cost; the listing tags ids this repository has already seen accepted or refused.",
|
|
1591
|
+
description: "For verify: the sync (non-batch) OpenRouter model to read with; defaults to the run's model without its :batch suffix. For submit: the OpenRouter batch model for this run (an id ending in :batch, as listed by action 'models'). Falls back to model in .codecarto/broadside/config.yaml, then the shipped default. Pre-flighted like the configured model: priced from the live catalog, refused without structured-output support, clamped to its completion ceiling. The models listing is advisory — some catalog ids have no batch endpoint and are refused at submit, at no cost; the listing tags ids this repository has already seen accepted or refused.",
|
|
1556
1592
|
},
|
|
1557
1593
|
lens_models: {
|
|
1558
1594
|
type: "object",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codecartographer-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.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",
|