codecartographer-pi 0.23.0 → 0.24.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/broadside/SKILL.md +44 -10
- 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/amendment.d.ts +6 -3
- package/dist/core/amendment.js +21 -10
- package/dist/core/broadside-verify.d.ts +95 -0
- package/dist/core/broadside-verify.js +433 -0
- package/dist/core/broadside.d.ts +45 -10
- package/dist/core/broadside.js +61 -16
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/status.d.ts +14 -0
- package/dist/core/status.js +104 -13
- package/dist/extensions/codecarto/agent-state.d.ts +7 -1
- package/dist/extensions/codecarto/agent-state.js +9 -1
- package/dist/extensions/codecarto/auto-runner.js +3 -2
- 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 +65 -7
- package/dist/mcp-server/server.d.ts +2 -1
- package/dist/mcp-server/server.js +50 -9
- package/package.json +1 -1
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED
package/dist/core/status.d.ts
CHANGED
|
@@ -2,6 +2,12 @@ import type { ClosureEntry, NormalizedStatus, OpenQuestionEntry, PostPipelineEnt
|
|
|
2
2
|
export declare const LOCK_RETRY_MS = 125;
|
|
3
3
|
export declare const LOCK_TIMEOUT_MS = 5000;
|
|
4
4
|
export declare const STALE_LOCK_MS = 60000;
|
|
5
|
+
/**
|
|
6
|
+
* How old the removal lock (`<lock>.break`, see {@link withRemovalLock}) may
|
|
7
|
+
* be before it is treated as left behind by a crashed process. It is held
|
|
8
|
+
* across one stat and one rm, so anything this old was abandoned.
|
|
9
|
+
*/
|
|
10
|
+
export declare const BREAK_LOCK_STALE_MS = 5000;
|
|
5
11
|
export declare function assertSafePhaseId(phaseId: string): void;
|
|
6
12
|
/**
|
|
7
13
|
* A YAML scalar as text: strings as written, numbers and booleans spelled
|
|
@@ -73,5 +79,13 @@ export interface LockHandle {
|
|
|
73
79
|
* the token, release removed whoever's lock was there: after a stale break
|
|
74
80
|
* the previous holder's release deleted the new holder's lock, and a third
|
|
75
81
|
* writer walked straight in (#227).
|
|
82
|
+
*
|
|
83
|
+
* Every removal — a release or a stale break — happens under the removal
|
|
84
|
+
* lock (`<lock>.break`) and re-checks what it is about to remove there.
|
|
85
|
+
* Two waiters that both saw a stale lock used to both `rm` it: the second
|
|
86
|
+
* `rm` landed after the first waiter had re-created the file, so both held
|
|
87
|
+
* the lock (#342). A file can only be created while the path is free, and
|
|
88
|
+
* only a removal-lock holder removes, so what a holder verified is what it
|
|
89
|
+
* removes.
|
|
76
90
|
*/
|
|
77
91
|
export declare function acquireLock(lockPath: string): Promise<LockHandle>;
|
package/dist/core/status.js
CHANGED
|
@@ -8,6 +8,12 @@ import { loadYamlFile } from "./yaml.js";
|
|
|
8
8
|
export const LOCK_RETRY_MS = 125;
|
|
9
9
|
export const LOCK_TIMEOUT_MS = 5000;
|
|
10
10
|
export const STALE_LOCK_MS = 60_000;
|
|
11
|
+
/**
|
|
12
|
+
* How old the removal lock (`<lock>.break`, see {@link withRemovalLock}) may
|
|
13
|
+
* be before it is treated as left behind by a crashed process. It is held
|
|
14
|
+
* across one stat and one rm, so anything this old was abandoned.
|
|
15
|
+
*/
|
|
16
|
+
export const BREAK_LOCK_STALE_MS = LOCK_TIMEOUT_MS;
|
|
11
17
|
export function assertSafePhaseId(phaseId) {
|
|
12
18
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(phaseId)) {
|
|
13
19
|
throw new Error(`Invalid phase id: ${phaseId}`);
|
|
@@ -434,6 +440,14 @@ export function applyHandoff(status, handoff) {
|
|
|
434
440
|
* the token, release removed whoever's lock was there: after a stale break
|
|
435
441
|
* the previous holder's release deleted the new holder's lock, and a third
|
|
436
442
|
* writer walked straight in (#227).
|
|
443
|
+
*
|
|
444
|
+
* Every removal — a release or a stale break — happens under the removal
|
|
445
|
+
* lock (`<lock>.break`) and re-checks what it is about to remove there.
|
|
446
|
+
* Two waiters that both saw a stale lock used to both `rm` it: the second
|
|
447
|
+
* `rm` landed after the first waiter had re-created the file, so both held
|
|
448
|
+
* the lock (#342). A file can only be created while the path is free, and
|
|
449
|
+
* only a removal-lock holder removes, so what a holder verified is what it
|
|
450
|
+
* removes.
|
|
437
451
|
*/
|
|
438
452
|
export async function acquireLock(lockPath) {
|
|
439
453
|
const startedAt = Date.now();
|
|
@@ -464,9 +478,13 @@ export async function acquireLock(lockPath) {
|
|
|
464
478
|
try {
|
|
465
479
|
const lockStat = await stat(lockPath);
|
|
466
480
|
if (Date.now() - lockStat.mtimeMs > STALE_LOCK_MS) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
481
|
+
const broken = await breakStaleLock(lockPath);
|
|
482
|
+
if (broken) {
|
|
483
|
+
brokeStale = broken;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
// Another waiter is breaking it, or already has: fall
|
|
487
|
+
// through to a wait and try the open again.
|
|
470
488
|
}
|
|
471
489
|
}
|
|
472
490
|
catch {
|
|
@@ -479,6 +497,70 @@ export async function acquireLock(lockPath) {
|
|
|
479
497
|
}
|
|
480
498
|
}
|
|
481
499
|
}
|
|
500
|
+
/**
|
|
501
|
+
* Run `remove` while holding `<lockPath>.break`, the lock that serializes
|
|
502
|
+
* removals of `lockPath`. Waits up to {@link LOCK_TIMEOUT_MS}; a removal lock
|
|
503
|
+
* older than {@link BREAK_LOCK_STALE_MS} is a crashed remover's and is
|
|
504
|
+
* cleared. Resolves to `undefined` when the removal lock could not be had
|
|
505
|
+
* in time — the caller decides what that means.
|
|
506
|
+
*/
|
|
507
|
+
async function withRemovalLock(lockPath, remove) {
|
|
508
|
+
const breakPath = `${lockPath}.break`;
|
|
509
|
+
const startedAt = Date.now();
|
|
510
|
+
while (true) {
|
|
511
|
+
try {
|
|
512
|
+
const handle = await open(breakPath, "wx");
|
|
513
|
+
await handle.close();
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
if (error.code !== "EEXIST")
|
|
518
|
+
throw error;
|
|
519
|
+
try {
|
|
520
|
+
const breakStat = await stat(breakPath);
|
|
521
|
+
if (Date.now() - breakStat.mtimeMs > BREAK_LOCK_STALE_MS) {
|
|
522
|
+
await rm(breakPath, { force: true }).catch(() => undefined);
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
if (Date.now() - startedAt > LOCK_TIMEOUT_MS)
|
|
530
|
+
return undefined;
|
|
531
|
+
await sleep(LOCK_RETRY_MS);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
return await remove();
|
|
536
|
+
}
|
|
537
|
+
finally {
|
|
538
|
+
await rm(breakPath, { force: true }).catch(() => undefined);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Remove a lock older than {@link STALE_LOCK_MS}, under the removal lock and
|
|
543
|
+
* only if it is still that old there: the holder may have released and a
|
|
544
|
+
* new one acquired between the caller's stat and this one. Resolves to the
|
|
545
|
+
* broken lock's holder, or null when nothing was removed.
|
|
546
|
+
*/
|
|
547
|
+
async function breakStaleLock(lockPath) {
|
|
548
|
+
const broken = await withRemovalLock(lockPath, async () => {
|
|
549
|
+
let lockStat;
|
|
550
|
+
try {
|
|
551
|
+
lockStat = await stat(lockPath);
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
if (Date.now() - lockStat.mtimeMs <= STALE_LOCK_MS)
|
|
557
|
+
return null;
|
|
558
|
+
const holder = await describeLockHolder(lockPath);
|
|
559
|
+
await rm(lockPath, { force: true }).catch(() => undefined);
|
|
560
|
+
return holder;
|
|
561
|
+
});
|
|
562
|
+
return broken ?? null;
|
|
563
|
+
}
|
|
482
564
|
/**
|
|
483
565
|
* Remove the lock at `lockPath` only if it is still ours. A lock that vanished
|
|
484
566
|
* (someone broke it as stale) or that now carries another holder's token is
|
|
@@ -486,16 +568,25 @@ export async function acquireLock(lockPath) {
|
|
|
486
568
|
* than removed unverified.
|
|
487
569
|
*/
|
|
488
570
|
async function releaseOwnedLock(lockPath, token) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
571
|
+
const removeIfOwned = async () => {
|
|
572
|
+
let content;
|
|
573
|
+
try {
|
|
574
|
+
content = await readFile(lockPath, "utf8");
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
if (content.split(/\r?\n/)[2] !== token)
|
|
580
|
+
return true;
|
|
581
|
+
await rm(lockPath, { force: true }).catch(() => undefined);
|
|
582
|
+
return true;
|
|
583
|
+
};
|
|
584
|
+
// Serialized with stale breaks so a break in progress cannot land on a
|
|
585
|
+
// lock this release has already replaced (#342). A removal lock that
|
|
586
|
+
// cannot be had in time falls back to the token-checked removal alone —
|
|
587
|
+
// the guarantee before #342, never less.
|
|
588
|
+
if ((await withRemovalLock(lockPath, removeIfOwned)) === undefined)
|
|
589
|
+
await removeIfOwned();
|
|
499
590
|
}
|
|
500
591
|
async function describeLockHolder(lockPath) {
|
|
501
592
|
try {
|
|
@@ -32,5 +32,11 @@ export declare function finishPhase(phaseId: string, outcome: {
|
|
|
32
32
|
* Clear a phase from the activity map. The widget (M2) will linger finished
|
|
33
33
|
* phases for a turn or two before calling this; for now (M1) we clear after
|
|
34
34
|
* a fixed timeout so the orchestrator notification stays meaningful.
|
|
35
|
+
*
|
|
36
|
+
* With `activity`, clear only while the map still holds that very entry.
|
|
37
|
+
* The runner's linger timer fires 30 s after a phase ends; a re-run of the
|
|
38
|
+
* same phase inside that window used to lose its live entry to the earlier
|
|
39
|
+
* run's timer — the widget dropped it and the re-entry guard let a second
|
|
40
|
+
* sub-agent start on the phase (#343).
|
|
35
41
|
*/
|
|
36
|
-
export declare function clearPhase(phaseId: string): void;
|
|
42
|
+
export declare function clearPhase(phaseId: string, activity?: PhaseActivity): void;
|
|
@@ -42,7 +42,15 @@ export function finishPhase(phaseId, outcome) {
|
|
|
42
42
|
* Clear a phase from the activity map. The widget (M2) will linger finished
|
|
43
43
|
* phases for a turn or two before calling this; for now (M1) we clear after
|
|
44
44
|
* a fixed timeout so the orchestrator notification stays meaningful.
|
|
45
|
+
*
|
|
46
|
+
* With `activity`, clear only while the map still holds that very entry.
|
|
47
|
+
* The runner's linger timer fires 30 s after a phase ends; a re-run of the
|
|
48
|
+
* same phase inside that window used to lose its live entry to the earlier
|
|
49
|
+
* run's timer — the widget dropped it and the re-entry guard let a second
|
|
50
|
+
* sub-agent start on the phase (#343).
|
|
45
51
|
*/
|
|
46
|
-
export function clearPhase(phaseId) {
|
|
52
|
+
export function clearPhase(phaseId, activity) {
|
|
53
|
+
if (activity && phaseActivity.get(phaseId) !== activity)
|
|
54
|
+
return;
|
|
47
55
|
phaseActivity.delete(phaseId);
|
|
48
56
|
}
|
|
@@ -132,8 +132,9 @@ export async function runSinglePhase(ctx, pi, state, phase, options) {
|
|
|
132
132
|
return { status: "error", activity, error: message };
|
|
133
133
|
}
|
|
134
134
|
finally {
|
|
135
|
-
// Linger 30s so /codecarto-status can show that the phase ran.
|
|
136
|
-
|
|
135
|
+
// Linger 30s so /codecarto-status can show that the phase ran. Tied
|
|
136
|
+
// to this run's entry: a re-run inside the window keeps its own (#343).
|
|
137
|
+
setTimeout(() => clearPhase(phase.id, activity), 30_000).unref?.();
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
/**
|
|
@@ -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,8 +10,8 @@ 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";
|
|
14
|
-
import { initLibrary } from "../../core/library.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
|
+
import { initLibrary, isValidSlug } from "../../core/library.js";
|
|
15
15
|
import { resolveUserConfigPath } from "../../core/orchestrator-config.js";
|
|
16
16
|
const STATUS_WIDGET_ID = "codecarto-widget";
|
|
17
17
|
// Broad-Side gets its own widget id: a scout run is legal on a repository with
|
|
@@ -995,7 +995,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
995
995
|
},
|
|
996
996
|
});
|
|
997
997
|
pi.registerCommand("codecarto-broadside", {
|
|
998
|
-
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]",
|
|
999
999
|
// Completes the token under the cursor, so lens names and flags are
|
|
1000
1000
|
// offered after the action too, and keeps everything typed before it.
|
|
1001
1001
|
getArgumentCompletions: (prefix) => completeLastToken(prefix, KNOWN_BROADSIDE_TOKENS.map((value) => ({ value }))),
|
|
@@ -1181,6 +1181,35 @@ export default function codeCartographerExtension(pi) {
|
|
|
1181
1181
|
}
|
|
1182
1182
|
return;
|
|
1183
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
|
+
}
|
|
1184
1213
|
// action === "collect"
|
|
1185
1214
|
renderProgress("Polling batches…");
|
|
1186
1215
|
try {
|
|
@@ -1325,12 +1354,41 @@ export default function codeCartographerExtension(pi) {
|
|
|
1325
1354
|
pi.registerCommand("codecarto-library-init", {
|
|
1326
1355
|
description: "Initialize a CodeCartographer library and configure it: /codecarto-library-init <path> [--namespace <name>]",
|
|
1327
1356
|
handler: async (args, ctx) => {
|
|
1328
|
-
const
|
|
1329
|
-
const
|
|
1357
|
+
const usage = "Usage: /codecarto-library-init <path> [--namespace <name>]";
|
|
1358
|
+
const parts = args.trim() === "" ? [] : args.trim().split(/\s+/);
|
|
1359
|
+
// `--namespace` with nothing after it used to read as "no namespace"
|
|
1360
|
+
// and initialize an unnamespaced library without a word (Broad-Side
|
|
1361
|
+
// verify, 2026-09-13 run): the flag is either complete or refused.
|
|
1330
1362
|
const namespaceIdx = parts.indexOf("--namespace");
|
|
1331
|
-
|
|
1363
|
+
let namespace = null;
|
|
1364
|
+
if (namespaceIdx >= 0) {
|
|
1365
|
+
const value = parts[namespaceIdx + 1];
|
|
1366
|
+
if (value === undefined || value.startsWith("--")) {
|
|
1367
|
+
notifyCtx(ctx, `--namespace needs a name. ${usage}`, "warning");
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
// The rule publish applies to it later, applied before it is
|
|
1371
|
+
// written into the config: lowercase ASCII, starts with a
|
|
1372
|
+
// letter, at most 64 characters.
|
|
1373
|
+
if (!isValidSlug(value)) {
|
|
1374
|
+
notifyCtx(ctx, `Invalid namespace "${value}" (lowercase ASCII, starts with a letter, max 64 chars). ${usage}`, "warning");
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
namespace = value;
|
|
1378
|
+
parts.splice(namespaceIdx, 2);
|
|
1379
|
+
}
|
|
1380
|
+
const stray = parts.find((part) => part.startsWith("--"));
|
|
1381
|
+
if (stray) {
|
|
1382
|
+
notifyCtx(ctx, `Unknown flag ${stray}. ${usage}`, "warning");
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
const [pathArg, ...extra] = parts;
|
|
1332
1386
|
if (!pathArg) {
|
|
1333
|
-
notifyCtx(ctx,
|
|
1387
|
+
notifyCtx(ctx, usage, "warning");
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
if (extra.length > 0) {
|
|
1391
|
+
notifyCtx(ctx, `One path, please — got ${parts.length}. ${usage}`, "warning");
|
|
1334
1392
|
return;
|
|
1335
1393
|
}
|
|
1336
1394
|
// The same expansion the config loader applies, so `~user/x` and a
|
|
@@ -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";
|
|
@@ -808,6 +808,11 @@ export async function handleLibraryInit(args) {
|
|
|
808
808
|
}
|
|
809
809
|
const libraryPath = args.library_path;
|
|
810
810
|
const namespaced = !!args.namespace;
|
|
811
|
+
// The rule codecarto_publish applies to the namespace later, applied
|
|
812
|
+
// before it is written into the config.
|
|
813
|
+
if (namespaced && !isValidSlug(args.namespace)) {
|
|
814
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid namespace "${args.namespace}" (lowercase ASCII, starts with a letter, max 64 chars).`);
|
|
815
|
+
}
|
|
811
816
|
const result = await initLibrary(libraryPath, {
|
|
812
817
|
name: args.name,
|
|
813
818
|
namespaced,
|
|
@@ -1037,8 +1042,8 @@ function resolveBroadsideApiKey(explicit, config) {
|
|
|
1037
1042
|
export async function handleBroadside(args) {
|
|
1038
1043
|
const cwd = await validateCwd(args.cwd);
|
|
1039
1044
|
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.`);
|
|
1045
|
+
if (!["submit", "collect", "status", "models", "verify"].includes(action)) {
|
|
1046
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown action: ${action}. Valid actions: submit, collect, status, models, verify.`);
|
|
1042
1047
|
}
|
|
1043
1048
|
// A config.yaml that exists but cannot be read refuses every action that
|
|
1044
1049
|
// would act on it (#232); status only reads recorded runs, so it answers
|
|
@@ -1173,8 +1178,40 @@ export async function handleBroadside(args) {
|
|
|
1173
1178
|
maxCost: result.maxCost,
|
|
1174
1179
|
});
|
|
1175
1180
|
}
|
|
1176
|
-
// action === "collect"
|
|
1177
1181
|
const runId = typeof args.run_id === "string" && args.run_id.trim() ? args.run_id.trim() : undefined;
|
|
1182
|
+
if (action === "verify") {
|
|
1183
|
+
// The verification pass (#143): one sync call per finding with read-only
|
|
1184
|
+
// tools, most severe first. `max_cost` is a running cap here — a sync
|
|
1185
|
+
// call's cost is known only when it returns — so the pass stops before
|
|
1186
|
+
// the next finding once reached; absent, config.yaml's cap applies.
|
|
1187
|
+
if (args.top !== undefined && !(typeof args.top === "number" && Number.isInteger(args.top) && args.top >= 1)) {
|
|
1188
|
+
throw new McpError(ErrorCode.InvalidParams, "top must be a positive integer.");
|
|
1189
|
+
}
|
|
1190
|
+
if (args.model !== undefined && !(typeof args.model === "string" && args.model.trim())) {
|
|
1191
|
+
throw new McpError(ErrorCode.InvalidParams, "model must be a non-empty OpenRouter model id.");
|
|
1192
|
+
}
|
|
1193
|
+
const maxCost = typeof args.max_cost === "number" && args.max_cost >= 0 ? args.max_cost : config.maxCost;
|
|
1194
|
+
const verified = await runBroadsideVerify(cwd, apiKey, {
|
|
1195
|
+
...(runId && { runId }),
|
|
1196
|
+
...(args.top !== undefined && { top: args.top }),
|
|
1197
|
+
...(typeof args.model === "string" && args.model.trim() && { model: args.model.trim() }),
|
|
1198
|
+
maxCost,
|
|
1199
|
+
signal: serverLifetime?.signal,
|
|
1200
|
+
}).catch((error) => {
|
|
1201
|
+
throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
|
|
1202
|
+
});
|
|
1203
|
+
return textResult(verifyResultText(verified), {
|
|
1204
|
+
runId: verified.runId,
|
|
1205
|
+
outputDir: verified.outputDir,
|
|
1206
|
+
status: verified.status,
|
|
1207
|
+
model: verified.model,
|
|
1208
|
+
candidates: verified.candidates,
|
|
1209
|
+
totalCost: verified.totalCost,
|
|
1210
|
+
...(verified.stoppedByCost && { stoppedByCost: true }),
|
|
1211
|
+
findings: verified.findings,
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
// action === "collect"
|
|
1178
1215
|
const collect = await runBroadsideCollect(cwd, apiKey, {
|
|
1179
1216
|
waitMs,
|
|
1180
1217
|
includeSynthesis,
|
|
@@ -1502,8 +1539,8 @@ const TOOLS = [
|
|
|
1502
1539
|
cwd: { type: "string", description: "Absolute path to the target repository." },
|
|
1503
1540
|
action: {
|
|
1504
1541
|
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.",
|
|
1542
|
+
enum: ["submit", "collect", "status", "models", "verify"],
|
|
1543
|
+
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
1544
|
},
|
|
1508
1545
|
lenses: {
|
|
1509
1546
|
type: "array",
|
|
@@ -1516,7 +1553,11 @@ const TOOLS = [
|
|
|
1516
1553
|
},
|
|
1517
1554
|
run_id: {
|
|
1518
1555
|
type: "string",
|
|
1519
|
-
description: "For collect: the run to
|
|
1556
|
+
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.",
|
|
1557
|
+
},
|
|
1558
|
+
top: {
|
|
1559
|
+
type: "integer",
|
|
1560
|
+
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
1561
|
},
|
|
1521
1562
|
wait_seconds: {
|
|
1522
1563
|
type: "number",
|
|
@@ -1536,7 +1577,7 @@ const TOOLS = [
|
|
|
1536
1577
|
},
|
|
1537
1578
|
max_cost: {
|
|
1538
1579
|
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.",
|
|
1580
|
+
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
1581
|
},
|
|
1541
1582
|
force: {
|
|
1542
1583
|
type: "boolean",
|
|
@@ -1552,7 +1593,7 @@ const TOOLS = [
|
|
|
1552
1593
|
},
|
|
1553
1594
|
model: {
|
|
1554
1595
|
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.",
|
|
1596
|
+
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
1597
|
},
|
|
1557
1598
|
lens_models: {
|
|
1558
1599
|
type: "object",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codecartographer-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.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",
|