codecartographer-pi 0.24.1 → 0.25.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 +16 -0
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/README.md +3 -2
- package/agent-skill/codecartographer/references/broadside.md +5 -1
- package/dist/core/broadside.d.ts +68 -6
- package/dist/core/broadside.js +178 -16
- package/dist/extensions/codecarto/auto-runner.d.ts +7 -0
- package/dist/extensions/codecarto/auto-runner.js +13 -0
- package/dist/extensions/codecarto/broadside-flags.d.ts +3 -1
- package/dist/extensions/codecarto/broadside-flags.js +13 -0
- package/dist/extensions/codecarto/index.js +4 -3
- package/dist/mcp-server/server.d.ts +1 -0
- package/dist/mcp-server/server.js +13 -1
- package/package.json +1 -1
|
@@ -35,6 +35,10 @@ not replace any phase; it tells phases where to look.
|
|
|
35
35
|
it describes), **discarded** (the claim is wrong about the code, with the
|
|
36
36
|
guard or line that shows it), or **unclear**. Start from the confirmed
|
|
37
37
|
ones; treat a discarded one as answered unless the reasoning is thin.
|
|
38
|
+
Then make sure the work order was built from it: `status` shows
|
|
39
|
+
`triage: completed (built from N verdicts)`; if it says `(no verdicts)`,
|
|
40
|
+
run `collect --regenerate` (`regenerate_post_passes: true`) before
|
|
41
|
+
reading `triage.md`.
|
|
38
42
|
Measured on this repository, the top twelve findings by severity were two
|
|
39
43
|
real defects and ten that a look at the guard, the caller, or the tsconfig
|
|
40
44
|
dismissed — the pass agreed with a reviewer on all twelve for about a cent
|
|
@@ -85,6 +89,7 @@ Broad-Side is an executable-surface feature. On the Pi extension:
|
|
|
85
89
|
/codecarto-broadside status # show recorded runs
|
|
86
90
|
/codecarto-broadside models # compare batch models
|
|
87
91
|
/codecarto-broadside verify --top=10 # read the top findings against the source
|
|
92
|
+
/codecarto-broadside collect --regenerate # rebuild synthesis and triage from the verdicts
|
|
88
93
|
```
|
|
89
94
|
|
|
90
95
|
On the MCP server:
|
|
@@ -95,6 +100,7 @@ codecarto_broadside {cwd, action: "collect"} # poll, save, synt
|
|
|
95
100
|
codecarto_broadside {cwd, action: "status"} # show recorded runs
|
|
96
101
|
codecarto_broadside {cwd, action: "models"} # compare batch models
|
|
97
102
|
codecarto_broadside {cwd, action: "verify", top: 10} # read the top findings against the source
|
|
103
|
+
codecarto_broadside {cwd, action: "collect", regenerate_post_passes: true} # rebuild synthesis and triage from the verdicts
|
|
98
104
|
```
|
|
99
105
|
|
|
100
106
|
The `models` action lists every `:batch` variant on OpenRouter — pricing per
|
|
@@ -139,6 +145,16 @@ cap here, since a sync call's cost is known only when it returns: the pass
|
|
|
139
145
|
stops before the next finding once the calls so far have reached it and
|
|
140
146
|
reports `partial`. About a cent a finding on the default model.
|
|
141
147
|
|
|
148
|
+
The post-passes read the verdicts when they exist: a synthesis or triage
|
|
149
|
+
built after a `verify` ranks the confirmed findings first, drops the
|
|
150
|
+
discarded ones, lists the not-a-defect ones apart in `omitted`, and says in
|
|
151
|
+
its summary how many verdicts it was built from; `status` and the collect
|
|
152
|
+
report show `(built from N verdicts)` or `(no verdicts)` on each pass. A run
|
|
153
|
+
collected before it was verified has a work order built from batch
|
|
154
|
+
severities alone — `collect --regenerate` (`regenerate_post_passes: true`)
|
|
155
|
+
resets the settled passes and runs them again with the verdicts, for another
|
|
156
|
+
post-pass pair's cost; a pass still in flight is left to finish.
|
|
157
|
+
|
|
142
158
|
Two caveats apply to any model you pick. The `models` action lists every id
|
|
143
159
|
OpenRouter advertises a `:batch` variant for, and many of those variants do not
|
|
144
160
|
exist — submitting one returns `does not have a :batch endpoint`, with nothing in
|
package/README.md
CHANGED
|
@@ -395,15 +395,16 @@ codecarto_broadside {cwd, action: "submit", lenses: [...]} # fire the batches
|
|
|
395
395
|
codecarto_broadside {cwd, action: "status"} # what is in flight
|
|
396
396
|
codecarto_broadside {cwd, action: "collect"} # poll, save, synthesize, triage
|
|
397
397
|
codecarto_broadside {cwd, action: "verify", top: 10} # read the top findings against the source
|
|
398
|
+
codecarto_broadside {cwd, action: "collect", regenerate_post_passes: true} # rebuild the report and work order from the verdicts
|
|
398
399
|
```
|
|
399
400
|
|
|
400
|
-
A third action, `verify`, reads the top defect and security findings of a collected run against the real source — one sync-priced call each with read-only tools (`read_file`, `grep`, `list_dir`) confined to the repository — and writes `verified.md` with a verdict per finding: **confirmed** (a reachable failure, with the trigger), **not-a-defect**, **discarded**, or **unclear**. Measured on this repository, the top twelve findings by severity were two real defects and ten claims a look at the guard or the caller dismissed; the pass agreed with a reviewer on all twelve for a cent a finding. Read `verified.md` before `triage.md
|
|
401
|
+
A third action, `verify`, reads the top defect and security findings of a collected run against the real source — one sync-priced call each with read-only tools (`read_file`, `grep`, `list_dir`) confined to the repository — and writes `verified.md` with a verdict per finding: **confirmed** (a reachable failure, with the trigger), **not-a-defect**, **discarded**, or **unclear**. Measured on this repository, the top twelve findings by severity were two real defects and ten claims a look at the guard or the caller dismissed; the pass agreed with a reviewer on all twelve for a cent a finding. Read `verified.md` before `triage.md` — and have `triage.md` built from it: the synthesis and triage passes rank on the verdicts when they exist (confirmed first, discarded dropped, not-a-defect listed apart), and `collect` with `regenerate_post_passes: true` (`--regenerate` on Pi) rebuilds both for a run that was collected before it was verified.
|
|
401
402
|
|
|
402
403
|
Submit and collect are separate because batch jobs routinely take tens of minutes; collect is resumable and picks up whatever is still in flight (`wait_seconds: 0`, the default, polls once and returns), and two collects on one run — a retried tool call, a second session — never pay for the synthesis, triage, or truncation retry twice: each is claimed in the run's state before it is submitted, and a collect whose client has gone away stops polling and submits nothing further. Submit prices the run from the collected file sizes against the model's live per-token pricing (cached 24h) and refuses when the estimate exceeds `max_cost` — $1.00 unless the config or the call sets another value, `0` for no limit — unless `force: true` is passed — a pre-flight estimate, not a runtime stop. Actual spend lands in each run's `run-meta.json`.
|
|
403
404
|
|
|
404
405
|
Repository defaults live in `.codecarto/broadside/config.yaml` (`model`, `api_key`, `default_lenses`, `max_cost`, `pricing` overrides, `lens_models`, `incremental`, `retry_truncated`, `include_synthesis`, `include_triage`, `wait_seconds`); an explicit tool parameter always wins. `lens_models` routes individual lenses to their own batch model — a stronger model changes security and defect findings far more than it changes an architecture map — and each override is priced, capability-checked, and clamped exactly like the default, with the estimate broken out per lens so a mixed-model run cannot be approved without seeing which lens costs what. CodeCartographer ships no stronger default: which model earns its price depends on your repository and budget, so compare candidates with the `models` action and choose — for one run with the `model` and `lens_models` parameters (Pi: `--model=ID`, `--lens-model=LENS:ID`), or for the repository in `config.yaml`. The `models` listing is advisory: OpenRouter's catalog returns a `:batch` id for some models its Batch API then refuses (`does not have a :batch endpoint`), at no cost, and nothing in the catalog tells them apart — so the listing tags the ids this repository's own submits have seen accepted or refused, and a refused lens says why in the submit report. `codecarto_skill {cwd, name: "broadside"}` returns the reading guide for a completed run, and unlike post-pipeline skills it is not gated on a finished pipeline.
|
|
405
406
|
|
|
406
|
-
On the Pi extension the same run is `/codecarto-broadside [submit|collect|status|models|verify] [lenses…] [--model=ID] [--lens-model=LENS:ID] [--top=N]`, with tab-completion for actions, lens names, and flags and live per-lens progress while batches poll. The two surfaces differ in one deliberate place: MCP cannot ask a human, so it refuses a run over `max_cost` until you pass `force`; Pi shows the per-lens breakdown and asks, and your approval *is* the force flag. Neither surface takes an API key as a command argument — a key typed into a slash command lands in the session transcript.
|
|
407
|
+
On the Pi extension the same run is `/codecarto-broadside [submit|collect|status|models|verify] [lenses…] [--model=ID] [--lens-model=LENS:ID] [--top=N] [--regenerate]`, with tab-completion for actions, lens names, and flags and live per-lens progress while batches poll. The two surfaces differ in one deliberate place: MCP cannot ask a human, so it refuses a run over `max_cost` until you pass `force`; Pi shows the per-lens breakdown and asks, and your approval *is* the force flag. Neither surface takes an API key as a command argument — a key typed into a slash command lands in the session transcript.
|
|
407
408
|
|
|
408
409
|
Broad-Side needs runtime code, so firing a run is an executable-surface feature: Pi and MCP have it, the pure drop-in template does not (it carries only the reading guide).
|
|
409
410
|
|
|
@@ -99,7 +99,11 @@ Results land in `.codecarto/broadside/<run>/`. If `verified.md` is there, read
|
|
|
99
99
|
it before anything else: `action: "verify"` has read the top defect and
|
|
100
100
|
security findings against the source with read-only tools and given each a
|
|
101
101
|
verdict (confirmed with its trigger, not-a-defect, discarded with the guard
|
|
102
|
-
that shows it, unclear).
|
|
102
|
+
that shows it, unclear). The post-passes rank on those verdicts when they
|
|
103
|
+
exist; a run collected before it was verified has a work order built from
|
|
104
|
+
batch severities alone (`status` says `(no verdicts)`), and
|
|
105
|
+
`action: "collect", regenerate_post_passes: true` rebuilds both passes from
|
|
106
|
+
the verdicts for another post-pass pair's cost. Then, in this order:
|
|
103
107
|
|
|
104
108
|
1. `synthesis.md` — executive summary, severity counts, top cross-lens
|
|
105
109
|
findings, per-module risk.
|
package/dist/core/broadside.d.ts
CHANGED
|
@@ -226,6 +226,12 @@ export type BroadsideSynthesisEntry = {
|
|
|
226
226
|
cost?: number;
|
|
227
227
|
/** Why the pass was retired, when the batch reported one. */
|
|
228
228
|
error?: string;
|
|
229
|
+
/**
|
|
230
|
+
* How many verification verdicts the pass was built from (#338): the
|
|
231
|
+
* `verified.json` a `verify` pass wrote before this pass was submitted.
|
|
232
|
+
* Absent when the pass was built from the lens findings alone.
|
|
233
|
+
*/
|
|
234
|
+
verdicts?: number;
|
|
229
235
|
};
|
|
230
236
|
/** One triage item — a scouting lead turned into a work-order entry. */
|
|
231
237
|
export type TriageItem = {
|
|
@@ -238,12 +244,8 @@ export type TriageItem = {
|
|
|
238
244
|
effort_estimate: string;
|
|
239
245
|
rationale: string;
|
|
240
246
|
};
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
status: "pending" | "submitted" | "completed" | "failed";
|
|
244
|
-
cost?: number;
|
|
245
|
-
error?: string;
|
|
246
|
-
};
|
|
247
|
+
/** The triage post-pass entry: the same shape as synthesis's. */
|
|
248
|
+
export type BroadsideTriageEntry = BroadsideSynthesisEntry;
|
|
247
249
|
/** Recorded on the run once a verification pass has run (#143); see core/broadside-verify.ts. */
|
|
248
250
|
export type BroadsideVerifyEntry = {
|
|
249
251
|
/** `completed`: every selected finding got a verdict; `partial`: the cost cap or an abort stopped it early. */
|
|
@@ -264,6 +266,8 @@ export type BroadsideRetryEntry = {
|
|
|
264
266
|
}>;
|
|
265
267
|
/** When the owning collect claimed the pass (#322). */
|
|
266
268
|
claimedAt: string;
|
|
269
|
+
/** What the retry batches cost, once polled to completion. */
|
|
270
|
+
cost?: number;
|
|
267
271
|
};
|
|
268
272
|
/**
|
|
269
273
|
* The parts of a run that cost money to submit and that exactly one collect
|
|
@@ -288,6 +292,11 @@ export type BroadsideRun = {
|
|
|
288
292
|
retry?: BroadsideRetryEntry;
|
|
289
293
|
/** The verification pass over the top findings, when one has run (#143). */
|
|
290
294
|
verify?: BroadsideVerifyEntry;
|
|
295
|
+
/**
|
|
296
|
+
* What post-pass results that were later regenerated had cost (#338):
|
|
297
|
+
* money the run spent that no current entry accounts for.
|
|
298
|
+
*/
|
|
299
|
+
retiredCost?: number;
|
|
291
300
|
totalCost?: number;
|
|
292
301
|
pricing?: ModelPricing;
|
|
293
302
|
maxCost?: number;
|
|
@@ -505,6 +514,8 @@ export type BroadsideCollectResult = {
|
|
|
505
514
|
summary: string;
|
|
506
515
|
}[];
|
|
507
516
|
topTriageItems: TriageItem[];
|
|
517
|
+
/** The post-passes this collect reset and re-ran on request (#338). */
|
|
518
|
+
regenerated?: Array<"synthesis" | "triage">;
|
|
508
519
|
};
|
|
509
520
|
type LensDefinition = {
|
|
510
521
|
id: BroadsideLensId;
|
|
@@ -680,6 +691,17 @@ export declare function persistBroadsideRunMerging(broadsideDir: string, run: Br
|
|
|
680
691
|
* flight elsewhere.
|
|
681
692
|
*/
|
|
682
693
|
export declare function claimRunSlot(broadsideDir: string, run: BroadsideRun, slot: BroadsideRunSlot): Promise<boolean>;
|
|
694
|
+
/**
|
|
695
|
+
* Put a run's settled post-passes back to `pending` on disk so the next
|
|
696
|
+
* claim re-runs them (#338). A pass another collect has in flight is left
|
|
697
|
+
* alone — its result is still coming. The replaced results' cost moves to
|
|
698
|
+
* `retiredCost`, so the run's total keeps counting money it spent. Returns
|
|
699
|
+
* the passes that were reset, in the order they will be re-run.
|
|
700
|
+
*/
|
|
701
|
+
export declare function resetRunPostPasses(broadsideDir: string, run: BroadsideRun, wanted: {
|
|
702
|
+
synthesis: boolean;
|
|
703
|
+
triage: boolean;
|
|
704
|
+
}): Promise<Array<"synthesis" | "triage">>;
|
|
683
705
|
export declare function loadBroadsideConfig(broadsideDir: string): Promise<BroadsideConfig>;
|
|
684
706
|
/** The shipped defaults: what an absent config.yaml means. */
|
|
685
707
|
export declare function defaultBroadsideConfig(): BroadsideConfig;
|
|
@@ -831,6 +853,39 @@ export declare function saveLensResults(runDir: string, lensId: BroadsideLensId,
|
|
|
831
853
|
* "a resumed collect can finish whichever is still pending" true.
|
|
832
854
|
*/
|
|
833
855
|
export declare function loadSavedLensResults(runDir: string, lenses: BroadsideLensId[]): Promise<StoredLensResult[]>;
|
|
856
|
+
/**
|
|
857
|
+
* One verdict from a run's `verified.json` (written by the verify pass in
|
|
858
|
+
* `broadside-verify.ts`), reduced to what the post-passes are told.
|
|
859
|
+
*/
|
|
860
|
+
export type PostPassVerdict = {
|
|
861
|
+
lensId: string;
|
|
862
|
+
customId: string;
|
|
863
|
+
severity: string;
|
|
864
|
+
title: string;
|
|
865
|
+
location: string;
|
|
866
|
+
verdict: string;
|
|
867
|
+
confidence: string;
|
|
868
|
+
evidence: Array<{
|
|
869
|
+
file: string;
|
|
870
|
+
lines: string;
|
|
871
|
+
note: string;
|
|
872
|
+
}>;
|
|
873
|
+
reasoning: string;
|
|
874
|
+
};
|
|
875
|
+
/**
|
|
876
|
+
* The verdicts a verify pass left in the run directory, or null when none
|
|
877
|
+
* has run (#338). A file that does not parse is treated as absent: the
|
|
878
|
+
* post-passes then run from the findings alone, which is what they did
|
|
879
|
+
* before verdicts existed, and `status` shows the pass carried no verdicts.
|
|
880
|
+
*/
|
|
881
|
+
export declare function loadPostPassVerdicts(runDir: string): Promise<PostPassVerdict[] | null>;
|
|
882
|
+
/**
|
|
883
|
+
* The verdicts as a section of the post-pass user message: one line per
|
|
884
|
+
* finding with the verdict, the evidence the verifier cited, and its
|
|
885
|
+
* reasoning, so the pass can rank on them rather than on the batch model's
|
|
886
|
+
* own severities (#338).
|
|
887
|
+
*/
|
|
888
|
+
export declare function renderPostPassVerdicts(verdicts: PostPassVerdict[]): string;
|
|
834
889
|
export declare function runBroadsideCollect(cwd: string, apiKey: string, opts?: {
|
|
835
890
|
waitMs?: number;
|
|
836
891
|
includeSynthesis?: boolean;
|
|
@@ -853,6 +908,13 @@ export declare function runBroadsideCollect(cwd: string, apiKey: string, opts?:
|
|
|
853
908
|
signal?: AbortSignal;
|
|
854
909
|
/** Poll cadence override; tests drive the loop faster than 15 s. */
|
|
855
910
|
pollIntervalMs?: number;
|
|
911
|
+
/**
|
|
912
|
+
* Reset the wanted post-passes of a collected run and run them again
|
|
913
|
+
* (#338) — after a `verify`, so the executive report and the work order
|
|
914
|
+
* are built from the verdicts. A pass still in flight is left to finish;
|
|
915
|
+
* a run whose lens batches are still running is refused.
|
|
916
|
+
*/
|
|
917
|
+
regeneratePostPasses?: boolean;
|
|
856
918
|
}): Promise<BroadsideCollectResult>;
|
|
857
919
|
export declare function runBroadsideStatus(cwd: string): Promise<{
|
|
858
920
|
state: BroadsideStateFile;
|
package/dist/core/broadside.js
CHANGED
|
@@ -1718,6 +1718,39 @@ export async function claimRunSlot(broadsideDir, run, slot) {
|
|
|
1718
1718
|
});
|
|
1719
1719
|
return owned;
|
|
1720
1720
|
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Put a run's settled post-passes back to `pending` on disk so the next
|
|
1723
|
+
* claim re-runs them (#338). A pass another collect has in flight is left
|
|
1724
|
+
* alone — its result is still coming. The replaced results' cost moves to
|
|
1725
|
+
* `retiredCost`, so the run's total keeps counting money it spent. Returns
|
|
1726
|
+
* the passes that were reset, in the order they will be re-run.
|
|
1727
|
+
*/
|
|
1728
|
+
export async function resetRunPostPasses(broadsideDir, run, wanted) {
|
|
1729
|
+
const reset = [];
|
|
1730
|
+
await updateBroadsideStateAtomically(broadsideDir, (state) => {
|
|
1731
|
+
const index = state.runs.findIndex((candidate) => candidate.id === run.id);
|
|
1732
|
+
const onDisk = index === -1 ? run : state.runs[index];
|
|
1733
|
+
for (const kind of ["synthesis", "triage"]) {
|
|
1734
|
+
if (!wanted[kind])
|
|
1735
|
+
continue;
|
|
1736
|
+
const theirs = onDisk[kind] ?? { status: "pending" };
|
|
1737
|
+
if (theirs.status !== "completed" && theirs.status !== "failed") {
|
|
1738
|
+
// pending: nothing to reset; submitted: in flight elsewhere.
|
|
1739
|
+
run[kind] = theirs;
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
if (theirs.cost)
|
|
1743
|
+
onDisk.retiredCost = (onDisk.retiredCost ?? 0) + theirs.cost;
|
|
1744
|
+
onDisk[kind] = { status: "pending" };
|
|
1745
|
+
run[kind] = onDisk[kind];
|
|
1746
|
+
run.retiredCost = onDisk.retiredCost;
|
|
1747
|
+
reset.push(kind);
|
|
1748
|
+
}
|
|
1749
|
+
if (index === -1)
|
|
1750
|
+
state.runs.push(run);
|
|
1751
|
+
});
|
|
1752
|
+
return reset;
|
|
1753
|
+
}
|
|
1721
1754
|
/** Read a `reasoning:` block from config.yaml, ignoring anything malformed. */
|
|
1722
1755
|
function parseReasoningConfig(raw) {
|
|
1723
1756
|
if (raw === false)
|
|
@@ -2722,8 +2755,86 @@ async function loadStoredRequests(runDir) {
|
|
|
2722
2755
|
return {};
|
|
2723
2756
|
}
|
|
2724
2757
|
}
|
|
2725
|
-
|
|
2726
|
-
|
|
2758
|
+
/**
|
|
2759
|
+
* The verdicts a verify pass left in the run directory, or null when none
|
|
2760
|
+
* has run (#338). A file that does not parse is treated as absent: the
|
|
2761
|
+
* post-passes then run from the findings alone, which is what they did
|
|
2762
|
+
* before verdicts existed, and `status` shows the pass carried no verdicts.
|
|
2763
|
+
*/
|
|
2764
|
+
export async function loadPostPassVerdicts(runDir) {
|
|
2765
|
+
const path = join(runDir, "verified.json");
|
|
2766
|
+
if (!(await pathExists(path)))
|
|
2767
|
+
return null;
|
|
2768
|
+
try {
|
|
2769
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
2770
|
+
const findings = Array.isArray(parsed.findings) ? parsed.findings : [];
|
|
2771
|
+
const verdicts = findings
|
|
2772
|
+
.filter((f) => typeof f.title === "string" && typeof f.verdict === "string")
|
|
2773
|
+
.map((f) => ({
|
|
2774
|
+
lensId: String(f.lensId ?? ""),
|
|
2775
|
+
customId: String(f.customId ?? ""),
|
|
2776
|
+
severity: String(f.severity ?? ""),
|
|
2777
|
+
title: String(f.title),
|
|
2778
|
+
location: String(f.location ?? ""),
|
|
2779
|
+
verdict: String(f.verdict),
|
|
2780
|
+
confidence: String(f.confidence ?? ""),
|
|
2781
|
+
evidence: Array.isArray(f.evidence)
|
|
2782
|
+
? f.evidence.map((e) => ({ file: String(e.file ?? ""), lines: String(e.lines ?? ""), note: String(e.note ?? "") }))
|
|
2783
|
+
: [],
|
|
2784
|
+
reasoning: String(f.reasoning ?? ""),
|
|
2785
|
+
}));
|
|
2786
|
+
return verdicts.length > 0 ? verdicts : null;
|
|
2787
|
+
}
|
|
2788
|
+
catch {
|
|
2789
|
+
return null;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
/**
|
|
2793
|
+
* The verdicts as a section of the post-pass user message: one line per
|
|
2794
|
+
* finding with the verdict, the evidence the verifier cited, and its
|
|
2795
|
+
* reasoning, so the pass can rank on them rather than on the batch model's
|
|
2796
|
+
* own severities (#338).
|
|
2797
|
+
*/
|
|
2798
|
+
export function renderPostPassVerdicts(verdicts) {
|
|
2799
|
+
const counts = new Map();
|
|
2800
|
+
for (const v of verdicts)
|
|
2801
|
+
counts.set(v.verdict, (counts.get(v.verdict) ?? 0) + 1);
|
|
2802
|
+
const tally = [...counts.entries()].map(([verdict, n]) => `${n} ${verdict}`).join(", ");
|
|
2803
|
+
const lines = [
|
|
2804
|
+
"",
|
|
2805
|
+
`## Verification verdicts (${verdicts.length} finding(s) read against the source by a read-only-tools pass: ${tally})`,
|
|
2806
|
+
"",
|
|
2807
|
+
"A verdict outranks the batch severity of the finding it names. `confirmed` means the verifier found a reachable " +
|
|
2808
|
+
"failure and named its trigger; `not-a-defect` means the claim is literally true of the code but nothing reaches the " +
|
|
2809
|
+
"failure it describes; `discarded` means the claim is wrong about the code; `unclear` means the code alone could not " +
|
|
2810
|
+
"settle it; `error` means the pass could not read it — treat that finding as unverified. Findings not listed here " +
|
|
2811
|
+
"were not read and stay unverified leads.",
|
|
2812
|
+
"",
|
|
2813
|
+
];
|
|
2814
|
+
for (const v of verdicts) {
|
|
2815
|
+
const evidence = v.evidence.map((e) => `${e.file}${e.lines ? `:${e.lines}` : ""}${e.note ? ` (${e.note})` : ""}`).join("; ");
|
|
2816
|
+
lines.push(`- [${v.verdict}${v.confidence ? `, ${v.confidence} confidence` : ""}] ${v.lensId}/${v.customId} — [${v.severity}] ${v.title}` +
|
|
2817
|
+
`${v.location ? ` @ ${v.location}` : ""}` +
|
|
2818
|
+
`${v.reasoning ? `\n Reasoning: ${v.reasoning.replace(/\s+/g, " ").trim()}` : ""}` +
|
|
2819
|
+
`${evidence ? `\n Evidence: ${evidence}` : ""}`);
|
|
2820
|
+
}
|
|
2821
|
+
lines.push("");
|
|
2822
|
+
return lines.join("\n");
|
|
2823
|
+
}
|
|
2824
|
+
const SYNTHESIS_VERDICT_INSTRUCTIONS = " A verification pass has read some of the findings against the source; its verdicts follow the reports. " +
|
|
2825
|
+
"Lead top_findings with the confirmed findings and begin each such summary with 'verified: confirmed — ' and the " +
|
|
2826
|
+
"trigger the verifier named; keep an unclear one with 'verified: unclear — '. A discarded or not-a-defect finding " +
|
|
2827
|
+
"does not appear in top_findings and is not counted in severity_summary. Say in the executive summary how many " +
|
|
2828
|
+
"findings were verified and how the verdicts split; findings the pass did not read remain unverified, and the " +
|
|
2829
|
+
"summary says so of them, not of the confirmed ones.";
|
|
2830
|
+
const TRIAGE_VERDICT_INSTRUCTIONS = " A verification pass has read some of the findings against the source; its verdicts follow the findings. " +
|
|
2831
|
+
"A confirmed finding ranks above every unverified finding of the same or lower severity: put the confirmed " +
|
|
2832
|
+
"findings at the top of the queue and begin each one's rationale with 'verified: confirmed — ' and the trigger " +
|
|
2833
|
+
"the verifier named. Keep an unclear finding in the queue with 'verified: unclear — ' in its rationale. Do not " +
|
|
2834
|
+
"queue a discarded or not-a-defect finding: list each in omitted, beginning with 'verified: discarded — ' or " +
|
|
2835
|
+
"'verified: not a defect — ' and the reason the pass gave. Findings the pass did not read stay unverified leads, " +
|
|
2836
|
+
"and the summary says how many verdicts the queue was built from.";
|
|
2837
|
+
function buildSynthesisRequest(findingsText, truncatedNote, model, verdicts = null) {
|
|
2727
2838
|
return {
|
|
2728
2839
|
custom_id: "synthesis",
|
|
2729
2840
|
body: {
|
|
@@ -2739,13 +2850,15 @@ function buildSynthesisRequest(findingsText, truncatedNote, model) {
|
|
|
2739
2850
|
"synthesis_report schema. Prioritize the most actionable findings. " +
|
|
2740
2851
|
"Be honest about gaps — if a lens found nothing, say 'no issues found' rather than " +
|
|
2741
2852
|
"inventing problems. These are scouting signals from a batch model, not verified " +
|
|
2742
|
-
"claims; note that in the summary."
|
|
2853
|
+
"claims; note that in the summary." +
|
|
2854
|
+
(verdicts ? SYNTHESIS_VERDICT_INSTRUCTIONS : ""),
|
|
2743
2855
|
},
|
|
2744
2856
|
{
|
|
2745
2857
|
role: "user",
|
|
2746
2858
|
content: "Synthesize these analysis reports into a single summary.\n\n" +
|
|
2747
2859
|
findingsText +
|
|
2748
2860
|
truncatedNote +
|
|
2861
|
+
(verdicts ? renderPostPassVerdicts(verdicts) : "") +
|
|
2749
2862
|
"\nReturn the synthesis_report JSON schema.",
|
|
2750
2863
|
},
|
|
2751
2864
|
],
|
|
@@ -2754,7 +2867,7 @@ function buildSynthesisRequest(findingsText, truncatedNote, model) {
|
|
|
2754
2867
|
},
|
|
2755
2868
|
};
|
|
2756
2869
|
}
|
|
2757
|
-
function buildTriageRequest(findingsText, truncatedNote, model) {
|
|
2870
|
+
function buildTriageRequest(findingsText, truncatedNote, model, verdicts = null) {
|
|
2758
2871
|
return {
|
|
2759
2872
|
custom_id: "triage",
|
|
2760
2873
|
body: {
|
|
@@ -2771,13 +2884,15 @@ function buildTriageRequest(findingsText, truncatedNote, model) {
|
|
|
2771
2884
|
"too vague to act on and record each drop in omitted with the reason. These findings " +
|
|
2772
2885
|
"are UNVERIFIED scouting signals from a cheap batch model: the queue is a starting " +
|
|
2773
2886
|
"point for re-verification, not a commitment — say so in the summary, and never " +
|
|
2774
|
-
"inflate a severity you cannot see evidence for."
|
|
2887
|
+
"inflate a severity you cannot see evidence for." +
|
|
2888
|
+
(verdicts ? TRIAGE_VERDICT_INSTRUCTIONS : ""),
|
|
2775
2889
|
},
|
|
2776
2890
|
{
|
|
2777
2891
|
role: "user",
|
|
2778
2892
|
content: "Triage these scouting findings into a prioritized work order.\n\n" +
|
|
2779
2893
|
findingsText +
|
|
2780
2894
|
truncatedNote +
|
|
2895
|
+
(verdicts ? renderPostPassVerdicts(verdicts) : "") +
|
|
2781
2896
|
"\nReturn the triage_report JSON schema.",
|
|
2782
2897
|
},
|
|
2783
2898
|
],
|
|
@@ -3020,7 +3135,10 @@ export async function runBroadsideCollect(cwd, apiKey, opts = {}) {
|
|
|
3020
3135
|
continue;
|
|
3021
3136
|
const group = byModel.get(model);
|
|
3022
3137
|
const usage = (batch.usage ?? {});
|
|
3023
|
-
|
|
3138
|
+
// Kept on the entry, not just added to this collect's running total:
|
|
3139
|
+
// a later collect on the run used to report a total without it.
|
|
3140
|
+
if (typeof usage.cost === "number")
|
|
3141
|
+
run.retry = { ...run.retry, cost: (run.retry?.cost ?? 0) + usage.cost };
|
|
3024
3142
|
const results = Array.isArray(batch.results) ? batch.results : [];
|
|
3025
3143
|
for (const result of results) {
|
|
3026
3144
|
const stored = group.slices.get(String(result.custom_id ?? ""));
|
|
@@ -3058,6 +3176,23 @@ export async function runBroadsideCollect(cwd, apiKey, opts = {}) {
|
|
|
3058
3176
|
let topTriageItems = [];
|
|
3059
3177
|
const wantSynthesis = opts.includeSynthesis !== false;
|
|
3060
3178
|
const wantTriage = opts.includeTriage !== false;
|
|
3179
|
+
// A regenerate resets the wanted, settled passes to pending on disk first —
|
|
3180
|
+
// the merging persist keeps whatever is further along on disk, so an
|
|
3181
|
+
// in-memory reset alone would be undone by the next persist (#338).
|
|
3182
|
+
let regenerated = [];
|
|
3183
|
+
if (opts.regeneratePostPasses) {
|
|
3184
|
+
if (!wantSynthesis && !wantTriage) {
|
|
3185
|
+
throw new Error("Nothing to regenerate: both post-passes are disabled for this collect.");
|
|
3186
|
+
}
|
|
3187
|
+
const lensesSettled = run.lenses.every((lensId) => {
|
|
3188
|
+
const entry = run.batches[lensId];
|
|
3189
|
+
return entry && BROADSIDE_TERMINAL_ENTRY_STATUSES.includes(entry.status);
|
|
3190
|
+
});
|
|
3191
|
+
if (!lensesSettled) {
|
|
3192
|
+
throw new Error(`Cannot regenerate the post-passes of run ${run.id}: its lens batches are still running — collect them first.`);
|
|
3193
|
+
}
|
|
3194
|
+
regenerated = await resetRunPostPasses(broadsideDir, run, { synthesis: wantSynthesis, triage: wantTriage });
|
|
3195
|
+
}
|
|
3061
3196
|
// A resumed collect polls nothing — every lens is already terminal — so the
|
|
3062
3197
|
// findings the post-passes need have to come back off disk, or a run whose
|
|
3063
3198
|
// first collect was interrupted could never produce its executive report
|
|
@@ -3080,6 +3215,9 @@ export async function runBroadsideCollect(cwd, apiKey, opts = {}) {
|
|
|
3080
3215
|
const findingsText = allLensResults
|
|
3081
3216
|
.map((r) => `## ${r.lensId} — ${r.customId}\n\n${r.content}\n`)
|
|
3082
3217
|
.join("\n");
|
|
3218
|
+
// A verify pass that ran before this point leaves its verdicts in the
|
|
3219
|
+
// run directory; the post-passes rank on them when present (#338).
|
|
3220
|
+
const verdicts = await loadPostPassVerdicts(runDir);
|
|
3083
3221
|
const truncatedNote = truncatedCount > 0
|
|
3084
3222
|
? `\n\nNOTE: ${truncatedCount} lens result(s) were truncated at the output token limit and are ` +
|
|
3085
3223
|
"not included above. Any gap they would have covered is unrepresented — do not treat " +
|
|
@@ -3101,12 +3239,17 @@ export async function runBroadsideCollect(cwd, apiKey, opts = {}) {
|
|
|
3101
3239
|
if (!(await claimRunSlot(broadsideDir, run, kind)))
|
|
3102
3240
|
continue;
|
|
3103
3241
|
owned.add(kind);
|
|
3242
|
+
const entry = kind === "synthesis" ? run.synthesis : run.triage;
|
|
3243
|
+
if (verdicts)
|
|
3244
|
+
entry.verdicts = verdicts.length;
|
|
3245
|
+
else
|
|
3246
|
+
delete entry.verdicts;
|
|
3104
3247
|
passes.push({
|
|
3105
3248
|
kind,
|
|
3106
3249
|
request: kind === "synthesis"
|
|
3107
|
-
? buildSynthesisRequest(findingsText, truncatedNote, run.model)
|
|
3108
|
-
: buildTriageRequest(findingsText, truncatedNote, run.model),
|
|
3109
|
-
entry
|
|
3250
|
+
? buildSynthesisRequest(findingsText, truncatedNote, run.model, verdicts)
|
|
3251
|
+
: buildTriageRequest(findingsText, truncatedNote, run.model, verdicts),
|
|
3252
|
+
entry,
|
|
3110
3253
|
});
|
|
3111
3254
|
}
|
|
3112
3255
|
const submitted = new Map();
|
|
@@ -3164,7 +3307,6 @@ export async function runBroadsideCollect(cwd, apiKey, opts = {}) {
|
|
|
3164
3307
|
const cost = typeof usage.cost === "number" ? usage.cost : undefined;
|
|
3165
3308
|
pass.entry.status = "completed";
|
|
3166
3309
|
pass.entry.cost = cost;
|
|
3167
|
-
totalCost += cost ?? 0;
|
|
3168
3310
|
const results = Array.isArray(batch.results) ? batch.results : [];
|
|
3169
3311
|
const content = results.length > 0 ? extractContent(results[0]) : null;
|
|
3170
3312
|
if (content !== null) {
|
|
@@ -3200,6 +3342,11 @@ export async function runBroadsideCollect(cwd, apiKey, opts = {}) {
|
|
|
3200
3342
|
return entry && BROADSIDE_TERMINAL_ENTRY_STATUSES.includes(entry.status);
|
|
3201
3343
|
});
|
|
3202
3344
|
run.status = terminal ? (resultCount > 0 ? "completed" : "failed") : "partial";
|
|
3345
|
+
// The run's total is the sum of what its entries record, not of what this
|
|
3346
|
+
// collect happened to poll: a repeat collect used to report — and persist
|
|
3347
|
+
// — a total without the post-passes and the retry an earlier collect had
|
|
3348
|
+
// settled, so the recorded cost of a run went down each time it was read.
|
|
3349
|
+
totalCost += (run.retry?.cost ?? 0) + (run.synthesis.cost ?? 0) + (run.triage.cost ?? 0) + (run.retiredCost ?? 0);
|
|
3203
3350
|
run.totalCost = totalCost;
|
|
3204
3351
|
await persist();
|
|
3205
3352
|
await writeFile(join(runDir, "run-meta.json"), `${JSON.stringify({
|
|
@@ -3239,6 +3386,7 @@ export async function runBroadsideCollect(cwd, apiKey, opts = {}) {
|
|
|
3239
3386
|
triage: run.triage,
|
|
3240
3387
|
topFindings,
|
|
3241
3388
|
topTriageItems,
|
|
3389
|
+
...(regenerated.length > 0 && { regenerated }),
|
|
3242
3390
|
};
|
|
3243
3391
|
}
|
|
3244
3392
|
export async function runBroadsideStatus(cwd) {
|
|
@@ -3521,10 +3669,17 @@ export function collectResultText(result) {
|
|
|
3521
3669
|
lines.push(` ${kind}: failed${entry.error ? ` — ${explainBatchError(entry.error)}` : ""}`);
|
|
3522
3670
|
}
|
|
3523
3671
|
};
|
|
3672
|
+
// Whether a pass was built from a verify pass's verdicts is part of what
|
|
3673
|
+
// it is: a work order that ranked on batch severities alone is the one
|
|
3674
|
+
// that put two dismissed casts above the confirmed finding (#338).
|
|
3675
|
+
const builtFrom = (entry) => entry.verdicts ? ` (built from ${entry.verdicts} verdict${entry.verdicts === 1 ? "" : "s"})` : " (no verdicts)";
|
|
3676
|
+
if (result.regenerated && result.regenerated.length > 0) {
|
|
3677
|
+
lines.push(` regenerated: ${result.regenerated.join(", ")}`);
|
|
3678
|
+
}
|
|
3524
3679
|
if (result.synthesis.status === "completed") {
|
|
3525
|
-
lines.push(` synthesis: completed, $${(result.synthesis.cost ?? 0).toFixed(6)}`);
|
|
3680
|
+
lines.push(` synthesis: completed, $${(result.synthesis.cost ?? 0).toFixed(6)}${builtFrom(result.synthesis)}`);
|
|
3526
3681
|
if (result.topFindings.length > 0) {
|
|
3527
|
-
lines.push("", "Top findings (unverified leads):");
|
|
3682
|
+
lines.push("", result.synthesis.verdicts ? "Top findings (verdicts applied; unread ones are unverified leads):" : "Top findings (unverified leads):");
|
|
3528
3683
|
for (const f of result.topFindings.slice(0, 10)) {
|
|
3529
3684
|
lines.push(` [${f.severity}] ${f.title}`);
|
|
3530
3685
|
}
|
|
@@ -3534,9 +3689,11 @@ export function collectResultText(result) {
|
|
|
3534
3689
|
passInFlight("synthesis", result.synthesis);
|
|
3535
3690
|
}
|
|
3536
3691
|
if (result.triage.status === "completed") {
|
|
3537
|
-
lines.push(` triage: completed, $${(result.triage.cost ?? 0).toFixed(6)}`);
|
|
3692
|
+
lines.push(` triage: completed, $${(result.triage.cost ?? 0).toFixed(6)}${builtFrom(result.triage)}`);
|
|
3538
3693
|
if (result.topTriageItems.length > 0) {
|
|
3539
|
-
lines.push("",
|
|
3694
|
+
lines.push("", result.triage.verdicts
|
|
3695
|
+
? "Triage — prioritized work order (confirmed findings first; re-verify the unread ones before acting):"
|
|
3696
|
+
: "Triage — prioritized work order (re-verify before acting):");
|
|
3540
3697
|
for (const item of result.topTriageItems.slice(0, 10)) {
|
|
3541
3698
|
lines.push(` ${item.priority} [${item.severity}/${item.module}] ${item.title}` +
|
|
3542
3699
|
(item.effort_estimate ? ` (${item.effort_estimate})` : ""));
|
|
@@ -3546,6 +3703,10 @@ export function collectResultText(result) {
|
|
|
3546
3703
|
else {
|
|
3547
3704
|
passInFlight("triage", result.triage);
|
|
3548
3705
|
}
|
|
3706
|
+
if (result.status === "completed" && !result.synthesis.verdicts && !result.triage.verdicts
|
|
3707
|
+
&& (result.synthesis.status === "completed" || result.triage.status === "completed")) {
|
|
3708
|
+
lines.push("", "Run verify, then collect --regenerate, to rebuild the report and the work order from verdicts.");
|
|
3709
|
+
}
|
|
3549
3710
|
lines.push("", "Disclaimer: Broad-Side findings are unverified scouting signals from a batch model, not validated claims.");
|
|
3550
3711
|
return lines.join("\n");
|
|
3551
3712
|
}
|
|
@@ -3585,8 +3746,9 @@ export function statusText(state) {
|
|
|
3585
3746
|
lines.push(` ${lensId}: ${entry.status}${entry.batchId ? ` (${entry.batchId})` : ""}${entry.cost !== undefined ? `, $${entry.cost.toFixed(6)}` : ""}` +
|
|
3586
3747
|
(entry.status === "skipped" && entry.reason ? ` — ${entry.reason}` : entry.fallback ? ` — ${entry.fallback}` : ""));
|
|
3587
3748
|
}
|
|
3588
|
-
|
|
3589
|
-
lines.push(`
|
|
3749
|
+
const builtFrom = (entry) => entry?.status === "completed" ? (entry.verdicts ? ` (built from ${entry.verdicts} verdict${entry.verdicts === 1 ? "" : "s"})` : " (no verdicts)") : "";
|
|
3750
|
+
lines.push(` synthesis: ${run.synthesis.status}${builtFrom(run.synthesis)}`);
|
|
3751
|
+
lines.push(` triage: ${run.triage?.status ?? "pending"}${builtFrom(run.triage)}`);
|
|
3590
3752
|
if (run.verify) {
|
|
3591
3753
|
lines.push(` verify: ${run.verify.status} — ${run.verify.confirmed} confirmed of ${run.verify.verified} read on ${run.verify.model}, $${run.verify.cost.toFixed(4)}`);
|
|
3592
3754
|
}
|
|
@@ -97,4 +97,11 @@ export type AutoDecision = {
|
|
|
97
97
|
};
|
|
98
98
|
export declare function decideAfterPhase(phaseStatus: SinglePhaseResult["status"], phaseError: string | undefined, validation: ValidationResult | null, strict: boolean): AutoDecision;
|
|
99
99
|
export declare function runAuto(ctx: ExtensionContext, pi: ExtensionAPI, initialState: WorkspaceState, options: AutoRunOptions): Promise<AutoRunResult>;
|
|
100
|
+
/**
|
|
101
|
+
* The one-line notification for an auto run's end. A run that stopped short
|
|
102
|
+
* carries its reason: the auto-summary message and the widget carry it too,
|
|
103
|
+
* but under `pi -p` neither is rendered, and "stopped: 0/1 phases" alone
|
|
104
|
+
* sent a reader back to the code to find out why (#347).
|
|
105
|
+
*/
|
|
106
|
+
export declare function describeAutoOutcome(result: AutoRunResult): string;
|
|
100
107
|
export declare function buildAutoSummary(result: AutoRunResult, availableSkills?: string[]): string;
|
|
@@ -305,6 +305,19 @@ export async function runAuto(ctx, pi, initialState, options) {
|
|
|
305
305
|
// ----------------------------------------------------------------------------
|
|
306
306
|
// buildAutoSummary — the codecarto-auto-summary message body
|
|
307
307
|
// ----------------------------------------------------------------------------
|
|
308
|
+
/**
|
|
309
|
+
* The one-line notification for an auto run's end. A run that stopped short
|
|
310
|
+
* carries its reason: the auto-summary message and the widget carry it too,
|
|
311
|
+
* but under `pi -p` neither is rendered, and "stopped: 0/1 phases" alone
|
|
312
|
+
* sent a reader back to the code to find out why (#347).
|
|
313
|
+
*/
|
|
314
|
+
export function describeAutoOutcome(result) {
|
|
315
|
+
const line = `Auto pipeline ${result.outcome}: ${result.phasesRun.length}/${result.totalPhases} phases.`;
|
|
316
|
+
if (result.outcome === "complete")
|
|
317
|
+
return line;
|
|
318
|
+
const reason = result.reason.trim();
|
|
319
|
+
return reason ? `${line.replace(/\.$/, "")} — ${reason}` : line;
|
|
320
|
+
}
|
|
308
321
|
export function buildAutoSummary(result, availableSkills = []) {
|
|
309
322
|
const totalTokens = result.totalTokens.input + result.totalTokens.output;
|
|
310
323
|
const wallTime = formatMillis(result.endedAt - result.startedAt);
|
|
@@ -24,11 +24,13 @@ export interface BroadsideFlags {
|
|
|
24
24
|
lensModels?: Partial<Record<BroadsideLensId, string>>;
|
|
25
25
|
/** For verify: how many findings to read (#143). */
|
|
26
26
|
top?: number;
|
|
27
|
+
/** For collect: reset the settled post-passes and run them again (#338). */
|
|
28
|
+
regeneratePostPasses?: boolean;
|
|
27
29
|
benchmarks: boolean;
|
|
28
30
|
unknown: string[];
|
|
29
31
|
/** Set on an invalid combination. The caller surfaces it as an error. */
|
|
30
32
|
error?: string;
|
|
31
33
|
}
|
|
32
34
|
/** Every token the completer offers, in the order it offers them. */
|
|
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"];
|
|
35
|
+
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=", "--regenerate", "--no-synthesis", "--no-triage", "--no-retry-truncated", "--benchmarks"];
|
|
34
36
|
export declare function parseBroadsideFlags(args: string): BroadsideFlags;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// /codecarto-broadside status
|
|
8
8
|
// /codecarto-broadside models --benchmarks
|
|
9
9
|
// /codecarto-broadside verify --top=10 → read the top findings against the source
|
|
10
|
+
// /codecarto-broadside collect --regenerate → rebuild synthesis and triage from the verdicts
|
|
10
11
|
//
|
|
11
12
|
// Flags mirror the codecarto_broadside tool parameters, with the negative
|
|
12
13
|
// forms spelled out because a slash command has no place to pass `false`:
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
// --model=ID (submit: the run's batch model, as listed by models; verify: the sync model to read with)
|
|
19
20
|
// --lens-model=LENS:ID (submit only, repeatable: one lens on its own model)
|
|
20
21
|
// --top=N (verify only: how many findings to read, most severe first)
|
|
22
|
+
// --regenerate (collect only: reset the settled post-passes and run them again, verdicts included)
|
|
21
23
|
//
|
|
22
24
|
// A model id itself contains a colon (`vendor/name:batch`), so --lens-model
|
|
23
25
|
// splits on the first colon only: `security:deepseek/deepseek-v4-pro:batch`.
|
|
@@ -47,6 +49,7 @@ export const KNOWN_BROADSIDE_TOKENS = [
|
|
|
47
49
|
"--model=",
|
|
48
50
|
"--lens-model=",
|
|
49
51
|
"--top=",
|
|
52
|
+
"--regenerate",
|
|
50
53
|
"--no-synthesis",
|
|
51
54
|
"--no-triage",
|
|
52
55
|
"--no-retry-truncated",
|
|
@@ -107,6 +110,10 @@ export function parseBroadsideFlags(args) {
|
|
|
107
110
|
result.retryTruncated = false;
|
|
108
111
|
continue;
|
|
109
112
|
}
|
|
113
|
+
if (token === "--regenerate") {
|
|
114
|
+
result.regeneratePostPasses = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
110
117
|
if (token === "--benchmarks") {
|
|
111
118
|
result.benchmarks = true;
|
|
112
119
|
continue;
|
|
@@ -188,6 +195,12 @@ export function parseBroadsideFlags(args) {
|
|
|
188
195
|
if (result.top !== undefined && result.action !== "verify") {
|
|
189
196
|
result.error ??= `--top is only meaningful for verify (got action "${result.action}").`;
|
|
190
197
|
}
|
|
198
|
+
if (result.regeneratePostPasses && result.action !== "collect") {
|
|
199
|
+
result.error ??= `--regenerate is only meaningful for collect (got action "${result.action}").`;
|
|
200
|
+
}
|
|
201
|
+
if (result.regeneratePostPasses && result.includeSynthesis === false && result.includeTriage === false) {
|
|
202
|
+
result.error ??= "--regenerate with both --no-synthesis and --no-triage leaves nothing to regenerate.";
|
|
203
|
+
}
|
|
191
204
|
if (result.action === "verify" && result.waitSeconds !== undefined) {
|
|
192
205
|
result.error ??= "--wait is only meaningful for submit and collect; verify runs to completion.";
|
|
193
206
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
-
import { autoCompletePhase, buildAutoSummary, isPhaseRunning, runAuto, runSinglePhase } from "./auto-runner.js";
|
|
3
|
+
import { autoCompletePhase, buildAutoSummary, describeAutoOutcome, isPhaseRunning, runAuto, runSinglePhase } from "./auto-runner.js";
|
|
4
4
|
import { disposeAgentsWidget } from "./agent-widget.js";
|
|
5
5
|
import { parseDashboardFlags } from "./dashboard-flags.js";
|
|
6
6
|
import { narrateDashboard } from "./dashboard-narrator.js";
|
|
@@ -638,7 +638,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
638
638
|
});
|
|
639
639
|
lastFeedbackLines = [`Auto pipeline ${result.outcome}: ${result.reason}`];
|
|
640
640
|
await refreshWorkspaceUi(ctx, lastFeedbackLines);
|
|
641
|
-
notifyCtx(ctx,
|
|
641
|
+
notifyCtx(ctx, describeAutoOutcome(result), result.outcome === "complete" ? "info" : "warning");
|
|
642
642
|
return;
|
|
643
643
|
}
|
|
644
644
|
const outcome = resolvePipelineOutcome(state);
|
|
@@ -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|verify] [lenses…] [--model=ID] [--lens-model=LENS:ID] [--top=N] [flags]",
|
|
998
|
+
description: "Batch reconnaissance (Broad-Side): /codecarto-broadside [submit|collect|status|models|verify] [lenses…] [--model=ID] [--lens-model=LENS:ID] [--top=N] [--regenerate] [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 }))),
|
|
@@ -1220,6 +1220,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
1220
1220
|
retryTruncated,
|
|
1221
1221
|
onStatus,
|
|
1222
1222
|
...(flags.runId && { runId: flags.runId }),
|
|
1223
|
+
...(flags.regeneratePostPasses && { regeneratePostPasses: true }),
|
|
1223
1224
|
});
|
|
1224
1225
|
const lines = collectResultText(collect).split("\n");
|
|
1225
1226
|
const done = collect.status === "completed";
|
|
@@ -1212,6 +1212,12 @@ export async function handleBroadside(args) {
|
|
|
1212
1212
|
});
|
|
1213
1213
|
}
|
|
1214
1214
|
// action === "collect"
|
|
1215
|
+
if (args.regenerate_post_passes !== undefined && typeof args.regenerate_post_passes !== "boolean") {
|
|
1216
|
+
throw new McpError(ErrorCode.InvalidParams, "regenerate_post_passes must be a boolean.");
|
|
1217
|
+
}
|
|
1218
|
+
if (args.regenerate_post_passes && !includeSynthesis && !includeTriage) {
|
|
1219
|
+
throw new McpError(ErrorCode.InvalidParams, "regenerate_post_passes needs at least one of include_synthesis and include_triage.");
|
|
1220
|
+
}
|
|
1215
1221
|
const collect = await runBroadsideCollect(cwd, apiKey, {
|
|
1216
1222
|
waitMs,
|
|
1217
1223
|
includeSynthesis,
|
|
@@ -1219,6 +1225,7 @@ export async function handleBroadside(args) {
|
|
|
1219
1225
|
retryTruncated,
|
|
1220
1226
|
signal: serverLifetime?.signal,
|
|
1221
1227
|
...(runId && { runId }),
|
|
1228
|
+
...(args.regenerate_post_passes && { regeneratePostPasses: true }),
|
|
1222
1229
|
}).catch((error) => {
|
|
1223
1230
|
throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
|
|
1224
1231
|
});
|
|
@@ -1232,6 +1239,7 @@ export async function handleBroadside(args) {
|
|
|
1232
1239
|
lensOutcomes: collect.lensOutcomes,
|
|
1233
1240
|
synthesis: collect.synthesis,
|
|
1234
1241
|
triage: collect.triage,
|
|
1242
|
+
...(collect.regenerated && { regenerated: collect.regenerated }),
|
|
1235
1243
|
topFindings: collect.topFindings,
|
|
1236
1244
|
topTriageItems: collect.topTriageItems,
|
|
1237
1245
|
});
|
|
@@ -1540,7 +1548,7 @@ const TOOLS = [
|
|
|
1540
1548
|
action: {
|
|
1541
1549
|
type: "string",
|
|
1542
1550
|
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.",
|
|
1551
|
+
description: "submit fires all lens batches and returns batch ids; collect polls submitted batches, saves results, and optionally runs the synthesis and triage passes — built from verified.json when a verify pass has already written it, so run verify first or collect again with regenerate_post_passes afterwards; 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.",
|
|
1544
1552
|
},
|
|
1545
1553
|
lenses: {
|
|
1546
1554
|
type: "array",
|
|
@@ -1575,6 +1583,10 @@ const TOOLS = [
|
|
|
1575
1583
|
type: "boolean",
|
|
1576
1584
|
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).",
|
|
1577
1585
|
},
|
|
1586
|
+
regenerate_post_passes: {
|
|
1587
|
+
type: "boolean",
|
|
1588
|
+
description: "collect only: reset the run's settled synthesis and triage passes and run them again, so a verify pass's verdicts (verified.json) are built into the executive report and the work order — confirmed findings first, discarded ones dropped, not-a-defect ones listed apart. Costs another post-pass pair (a few cents); a pass still in flight is left to finish; a run whose lens batches are still running is refused.",
|
|
1589
|
+
},
|
|
1578
1590
|
max_cost: {
|
|
1579
1591
|
type: "number",
|
|
1580
1592
|
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.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codecartographer-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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",
|