codecartographer-pi 0.22.0 → 0.22.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 +8 -5
- package/.codecarto/broadside/config.yaml +41 -20
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/dist/core/broadside.d.ts +42 -20
- package/dist/core/broadside.js +117 -30
- package/dist/extensions/codecarto/index.js +32 -8
- package/dist/mcp-server/server.js +5 -2
- package/package.json +1 -1
|
@@ -95,11 +95,14 @@ OpenRouter advertises a `:batch` variant for, and many of those variants do not
|
|
|
95
95
|
exist — submitting one returns `does not have a :batch endpoint`, with nothing in
|
|
96
96
|
the catalog to distinguish it beforehand. A rejected batch costs nothing, so
|
|
97
97
|
probe a candidate on a single lens first. And reasoning competes with the answer for
|
|
98
|
-
`max_tokens`: Broad-Side
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
`max_tokens`: Broad-Side asks every model for low reasoning effort so the output
|
|
99
|
+
budget stays with the JSON, which is the split the cost estimate already
|
|
100
|
+
assumes. An effort level rather than a token cap, because Gemini 3.x ignores a
|
|
101
|
+
cap (measured: 11,518 thinking tokens under a 5,800 cap) and honours the level;
|
|
102
|
+
a level rather than off, because some endpoints refuse to be switched off. A
|
|
103
|
+
slice that still truncates is retried once with a doubled budget at low effort.
|
|
104
|
+
Override with `reasoning:` in `config.yaml` only alongside a raised output
|
|
105
|
+
budget, and set `effort` or `max_tokens`, never both.
|
|
103
106
|
|
|
104
107
|
Lenses do not all have to run on the same model. `lens_models` in `config.yaml`
|
|
105
108
|
routes individual lenses to their own batch model — the usual reason being that
|
|
@@ -72,29 +72,50 @@
|
|
|
72
72
|
|
|
73
73
|
# Reasoning control, sent on every lens request.
|
|
74
74
|
#
|
|
75
|
-
# By default Broad-Side
|
|
76
|
-
# leaving the
|
|
77
|
-
# the cost estimate already assumes.
|
|
78
|
-
#
|
|
79
|
-
# The
|
|
80
|
-
# One measured run spent 5,758 of a 6,000-token budget thinking
|
|
81
|
-
# tokens for the JSON, which truncated mid-structure on 11 of 13
|
|
82
|
-
# tokens bill at the full output rate, so it paid for ~6,000
|
|
83
|
-
# slice to receive ~230 usable ones. The shipped default
|
|
84
|
-
# thing less consistently — reasoning from 0 to 5,757
|
|
85
|
-
# three of them cut off — so this is not something
|
|
86
|
-
#
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
75
|
+
# By default Broad-Side asks every model for low reasoning effort
|
|
76
|
+
# (`effort: low`), leaving the output budget for the answer — which is the
|
|
77
|
+
# split the cost estimate already assumes.
|
|
78
|
+
#
|
|
79
|
+
# The control exists because reasoning competes with the answer for
|
|
80
|
+
# `max_tokens`. One measured run spent 5,758 of a 6,000-token budget thinking
|
|
81
|
+
# and left ~230 tokens for the JSON, which truncated mid-structure on 11 of 13
|
|
82
|
+
# slices; those tokens bill at the full output rate, so it paid for ~6,000
|
|
83
|
+
# output tokens per slice to receive ~230 usable ones. The shipped default
|
|
84
|
+
# model does the same thing less consistently — reasoning from 0 to 5,757
|
|
85
|
+
# tokens across 13 slices, three of them cut off — so this is not something
|
|
86
|
+
# only exotic models do.
|
|
87
|
+
#
|
|
88
|
+
# It is an effort level rather than a token cap because a cap is not honoured
|
|
89
|
+
# everywhere. Gemini 3.x models take a thinking *level*, not a budget: under
|
|
90
|
+
# `max_tokens: 5800`, google/gemini-3.8-flash:batch reasoned 5,218 tokens on
|
|
91
|
+
# the first pass and 11,518 on the doubled-budget retry — the cap changed
|
|
92
|
+
# nothing, both results truncated, and the retry cost twice the original for
|
|
93
|
+
# no JSON. The same lens at `effort: low` reasoned 0 tokens, returned valid
|
|
94
|
+
# JSON, and cost a twelfth as much. `effort` is what OpenRouter can translate
|
|
95
|
+
# for every provider; a token cap reaches only the providers that take one.
|
|
96
|
+
#
|
|
97
|
+
# And it is a level rather than an off switch on purpose. Some endpoints refuse
|
|
98
|
+
# to be switched off: `google/gemini-3.8-flash:batch` rejects the entire batch
|
|
99
|
+
# with "Reasoning is mandatory for this endpoint and cannot be disabled", which
|
|
100
|
+
# turns a partial result into none at all. Low effort works either way.
|
|
101
|
+
#
|
|
102
|
+
# A slice that still truncates is re-submitted once with a doubled `max_tokens`
|
|
103
|
+
# and, whatever this block says, low effort — the cutoff is usually thinking.
|
|
104
|
+
#
|
|
105
|
+
# Set `effort` OR `max_tokens`, not both: OpenRouter refuses a request that
|
|
106
|
+
# carries both ("Only one of reasoning.effort and reasoning.max_tokens can be
|
|
107
|
+
# specified") — per request, after the batch is accepted, so every lens fails
|
|
108
|
+
# at $0. Submit refuses a config.yaml that sets both. Raise the effort only
|
|
109
|
+
# together with a raised lens output budget, or the JSON truncates exactly as
|
|
110
|
+
# above.
|
|
94
111
|
#
|
|
95
112
|
# reasoning:
|
|
96
113
|
# effort: low # minimal | low | medium | high
|
|
97
|
-
#
|
|
114
|
+
#
|
|
115
|
+
# reasoning:
|
|
116
|
+
# max_tokens: 2000 # a thinking budget, where the provider honours one
|
|
117
|
+
#
|
|
118
|
+
# reasoning:
|
|
98
119
|
# enabled: false # only where the provider allows it
|
|
99
120
|
|
|
100
121
|
# Approximate run expense limit in USD. The default is 1.00, and it applies
|
package/dist/core/broadside.d.ts
CHANGED
|
@@ -137,31 +137,43 @@ export type BroadsideReasoning = {
|
|
|
137
137
|
max_tokens?: number;
|
|
138
138
|
};
|
|
139
139
|
/**
|
|
140
|
-
* The
|
|
140
|
+
* The reasoning control every lens request carries: low effort.
|
|
141
141
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
142
|
+
* It used to be a token cap — `max_tokens` at a quarter of the lens's output
|
|
143
|
+
* budget, so three quarters stayed for the answer. Measured live on
|
|
144
|
+
* `google/gemini-3.8-flash:batch` (0.22.0 verification, defect lens, cap
|
|
145
|
+
* 5,800 of a 6,000 budget): the model reasoned 5,218 tokens on the first
|
|
146
|
+
* pass and **11,518 under the same cap** on the doubled-budget retry —
|
|
147
|
+
* thinking scaled with `max_tokens` and the cap changed nothing, both
|
|
148
|
+
* results truncated, and the retry cost twice the original for no JSON.
|
|
149
|
+
* The same lens with `effort: "low"` reasoned 0 tokens, finished with
|
|
150
|
+
* `stop`, returned valid JSON, and cost a twelfth as much. Gemini 3.x
|
|
151
|
+
* models take a thinking *level*, not a budget, and OpenRouter forwards a
|
|
152
|
+
* `max_tokens` cap to them as nothing at all; `effort` is what it can
|
|
153
|
+
* translate for every provider (a level where the provider has levels, a
|
|
154
|
+
* fraction of the budget where it takes a budget). So the default asks for
|
|
155
|
+
* little thinking in the one vocabulary that reaches everyone.
|
|
156
|
+
*
|
|
157
|
+
* Deliberately not `enabled: false`: `google/gemini-3.8-flash:batch` refuses
|
|
158
|
+
* the whole batch with *"Reasoning is mandatory for this endpoint and cannot
|
|
159
|
+
* be disabled"*, turning a partial result into none at all. Low effort works
|
|
160
|
+
* whether or not a provider allows reasoning to be switched off.
|
|
145
161
|
*/
|
|
146
|
-
export declare const
|
|
147
|
-
|
|
162
|
+
export declare const BROADSIDE_DEFAULT_REASONING: Readonly<BroadsideReasoning>;
|
|
163
|
+
/** The reasoning control a lens request carries when config.yaml sets none. */
|
|
164
|
+
export declare function defaultReasoningFor(): BroadsideReasoning;
|
|
148
165
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* Disabling outright is not portable: `google/gemini-3.8-flash:batch` refuses
|
|
152
|
-
* the whole batch with *"Reasoning is mandatory for this endpoint and cannot be
|
|
153
|
-
* disabled"*, turning a partial result into none at all. Capping works whether
|
|
154
|
-
* or not a provider allows reasoning to be switched off.
|
|
166
|
+
* The reasoning control a truncated slice is re-submitted with.
|
|
155
167
|
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
168
|
+
* A truncation on a reasoning-capable model is usually thinking that ate the
|
|
169
|
+
* answer's budget, and doubling `max_tokens` doubles the thinking where the
|
|
170
|
+
* provider ignores a token cap (see {@link BROADSIDE_DEFAULT_REASONING}). The
|
|
171
|
+
* retry therefore asks for low effort as well, replacing a `max_tokens` cap
|
|
172
|
+
* (OpenRouter refuses a request carrying both) and lowering a higher effort.
|
|
173
|
+
* An explicit `enabled: false` and an effort already at or below low are left
|
|
174
|
+
* as they are.
|
|
163
175
|
*/
|
|
164
|
-
export declare function
|
|
176
|
+
export declare function retryReasoningFor(original: BroadsideReasoning | undefined): BroadsideReasoning;
|
|
165
177
|
export type BatchRequest = {
|
|
166
178
|
custom_id: string;
|
|
167
179
|
body: {
|
|
@@ -189,6 +201,8 @@ export type BroadsideBatchEntry = {
|
|
|
189
201
|
cost?: number;
|
|
190
202
|
resultCount?: number;
|
|
191
203
|
error?: unknown;
|
|
204
|
+
/** Why a `skipped` lens had nothing to submit: the globs that matched no file. */
|
|
205
|
+
reason?: string;
|
|
192
206
|
/** Set when this lens used a model other than the run default. */
|
|
193
207
|
model?: string;
|
|
194
208
|
/** The completion ceiling of this lens's model; bounds the truncation retry. */
|
|
@@ -704,6 +718,7 @@ export declare function runBroadsideStatus(cwd: string): Promise<{
|
|
|
704
718
|
state: BroadsideStateFile;
|
|
705
719
|
}>;
|
|
706
720
|
export declare function renderFindingsMarkdown(content: string): string;
|
|
721
|
+
export declare function describeIncrementalFallback(reason: BroadsideIncrementalOutcome["reason"]): string;
|
|
707
722
|
export declare function estimateSubmitText(result: BroadsideSubmitResult, lenses: LensDefinition[]): string;
|
|
708
723
|
export declare function modelsText(entries: CatalogEntry[], opts: {
|
|
709
724
|
benchmarks: CodingBenchmarks | null;
|
|
@@ -724,5 +739,12 @@ export declare function modelsText(entries: CatalogEntry[], opts: {
|
|
|
724
739
|
*/
|
|
725
740
|
export declare function explainBatchError(error: unknown): string | null;
|
|
726
741
|
export declare function collectResultText(result: BroadsideCollectResult): string;
|
|
742
|
+
/**
|
|
743
|
+
* An `onStatus` callback that appends one line to `lines` per *change* of a
|
|
744
|
+
* lens's polled status. Every poll used to append a line, so a four-minute
|
|
745
|
+
* wait returned twenty-six identical "in_progress (0/1)" lines per lens
|
|
746
|
+
* before the result (0.22.0 live run).
|
|
747
|
+
*/
|
|
748
|
+
export declare function statusLineWriter(lines: string[]): (lensId: string, status: string, counts: Record<string, unknown>) => void;
|
|
727
749
|
export declare function statusText(state: BroadsideStateFile): string;
|
|
728
750
|
export {};
|
package/dist/core/broadside.js
CHANGED
|
@@ -94,32 +94,51 @@ export const BROADSIDE_DEFAULT_POLL_BUDGET_MS = 25 * 60 * 1000;
|
|
|
94
94
|
*/
|
|
95
95
|
export const BROADSIDE_DEFAULT_MAX_COST = 1;
|
|
96
96
|
/**
|
|
97
|
-
* The
|
|
97
|
+
* The reasoning control every lens request carries: low effort.
|
|
98
98
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
99
|
+
* It used to be a token cap — `max_tokens` at a quarter of the lens's output
|
|
100
|
+
* budget, so three quarters stayed for the answer. Measured live on
|
|
101
|
+
* `google/gemini-3.8-flash:batch` (0.22.0 verification, defect lens, cap
|
|
102
|
+
* 5,800 of a 6,000 budget): the model reasoned 5,218 tokens on the first
|
|
103
|
+
* pass and **11,518 under the same cap** on the doubled-budget retry —
|
|
104
|
+
* thinking scaled with `max_tokens` and the cap changed nothing, both
|
|
105
|
+
* results truncated, and the retry cost twice the original for no JSON.
|
|
106
|
+
* The same lens with `effort: "low"` reasoned 0 tokens, finished with
|
|
107
|
+
* `stop`, returned valid JSON, and cost a twelfth as much. Gemini 3.x
|
|
108
|
+
* models take a thinking *level*, not a budget, and OpenRouter forwards a
|
|
109
|
+
* `max_tokens` cap to them as nothing at all; `effort` is what it can
|
|
110
|
+
* translate for every provider (a level where the provider has levels, a
|
|
111
|
+
* fraction of the budget where it takes a budget). So the default asks for
|
|
112
|
+
* little thinking in the one vocabulary that reaches everyone.
|
|
113
|
+
*
|
|
114
|
+
* Deliberately not `enabled: false`: `google/gemini-3.8-flash:batch` refuses
|
|
115
|
+
* the whole batch with *"Reasoning is mandatory for this endpoint and cannot
|
|
116
|
+
* be disabled"*, turning a partial result into none at all. Low effort works
|
|
117
|
+
* whether or not a provider allows reasoning to be switched off.
|
|
102
118
|
*/
|
|
103
|
-
export const
|
|
104
|
-
|
|
119
|
+
export const BROADSIDE_DEFAULT_REASONING = Object.freeze({ effort: "low" });
|
|
120
|
+
/** The reasoning control a lens request carries when config.yaml sets none. */
|
|
121
|
+
export function defaultReasoningFor() {
|
|
122
|
+
return { ...BROADSIDE_DEFAULT_REASONING };
|
|
123
|
+
}
|
|
105
124
|
/**
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* Disabling outright is not portable: `google/gemini-3.8-flash:batch` refuses
|
|
109
|
-
* the whole batch with *"Reasoning is mandatory for this endpoint and cannot be
|
|
110
|
-
* disabled"*, turning a partial result into none at all. Capping works whether
|
|
111
|
-
* or not a provider allows reasoning to be switched off.
|
|
125
|
+
* The reasoning control a truncated slice is re-submitted with.
|
|
112
126
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
127
|
+
* A truncation on a reasoning-capable model is usually thinking that ate the
|
|
128
|
+
* answer's budget, and doubling `max_tokens` doubles the thinking where the
|
|
129
|
+
* provider ignores a token cap (see {@link BROADSIDE_DEFAULT_REASONING}). The
|
|
130
|
+
* retry therefore asks for low effort as well, replacing a `max_tokens` cap
|
|
131
|
+
* (OpenRouter refuses a request carrying both) and lowering a higher effort.
|
|
132
|
+
* An explicit `enabled: false` and an effort already at or below low are left
|
|
133
|
+
* as they are.
|
|
120
134
|
*/
|
|
121
|
-
export function
|
|
122
|
-
|
|
135
|
+
export function retryReasoningFor(original) {
|
|
136
|
+
if (original?.enabled === false)
|
|
137
|
+
return { ...original };
|
|
138
|
+
if (original?.effort === "minimal" || original?.effort === "low")
|
|
139
|
+
return { ...original };
|
|
140
|
+
const { max_tokens: _cap, effort: _effort, ...rest } = original ?? {};
|
|
141
|
+
return { ...rest, effort: "low" };
|
|
123
142
|
}
|
|
124
143
|
/**
|
|
125
144
|
* OpenRouter rejected the API key (HTTP 401/403). Thrown from the catalog
|
|
@@ -1348,7 +1367,7 @@ export function buildBatchRequest(lens, info, slice, index, sliceCount, model =
|
|
|
1348
1367
|
max_tokens: maxTokensOverride ?? lens.maxTokens,
|
|
1349
1368
|
// Always sent, never inherited: an absent field means the model's
|
|
1350
1369
|
// own default, and that default is what truncated the JSON.
|
|
1351
|
-
reasoning: reasoningOverride ?? lens.reasoning ?? defaultReasoningFor(
|
|
1370
|
+
reasoning: reasoningOverride ?? lens.reasoning ?? defaultReasoningFor(),
|
|
1352
1371
|
},
|
|
1353
1372
|
};
|
|
1354
1373
|
}
|
|
@@ -1549,6 +1568,21 @@ export async function loadBroadsideConfig(broadsideDir) {
|
|
|
1549
1568
|
throw new BroadsideConfigError(configPath, "is not a YAML mapping");
|
|
1550
1569
|
raw = parsed;
|
|
1551
1570
|
}
|
|
1571
|
+
// OpenRouter accepts `reasoning.effort` or `reasoning.max_tokens`, not
|
|
1572
|
+
// both: a request carrying both is refused per request *after* the batch
|
|
1573
|
+
// is accepted, so every lens fails at $0 with the reason in each
|
|
1574
|
+
// result's error. Seen live on 0.22.0 with the two keys set together.
|
|
1575
|
+
// Refuse here, where the file can be fixed, rather than submit a run
|
|
1576
|
+
// that cannot produce a result.
|
|
1577
|
+
const reasoning = raw.reasoning;
|
|
1578
|
+
if (reasoning && typeof reasoning === "object" && !Array.isArray(reasoning)) {
|
|
1579
|
+
const value = reasoning;
|
|
1580
|
+
const hasEffort = typeof value.effort === "string";
|
|
1581
|
+
const hasBudget = typeof value.max_tokens === "number" && value.max_tokens > 0;
|
|
1582
|
+
if (hasEffort && hasBudget) {
|
|
1583
|
+
throw new BroadsideConfigError(configPath, 'sets both reasoning.effort and reasoning.max_tokens; OpenRouter accepts one or the other ("Only one of reasoning.effort and reasoning.max_tokens can be specified"), and every lens request would fail after the batch is accepted. Keep one');
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1552
1586
|
}
|
|
1553
1587
|
return buildBroadsideConfig(raw);
|
|
1554
1588
|
}
|
|
@@ -2132,6 +2166,8 @@ export async function runBroadsideSubmit(cwd, apiKey, opts = {}) {
|
|
|
2132
2166
|
}
|
|
2133
2167
|
// Slice offline first so the estimate covers every request we would send.
|
|
2134
2168
|
const slicesByLens = new Map();
|
|
2169
|
+
// Why a lens ended up with nothing to submit, for the report (see below).
|
|
2170
|
+
const skipReasons = new Map();
|
|
2135
2171
|
let estimatedInputTokens = 0;
|
|
2136
2172
|
let estimatedOutputTokens = 0;
|
|
2137
2173
|
let estimatedTotalCost = 0;
|
|
@@ -2149,12 +2185,21 @@ export async function runBroadsideSubmit(cwd, apiKey, opts = {}) {
|
|
|
2149
2185
|
for (const file of slice.redactedFiles ?? [])
|
|
2150
2186
|
redactedFiles.add(file);
|
|
2151
2187
|
}
|
|
2188
|
+
const matchedBeforeIncremental = slices.length;
|
|
2152
2189
|
if (changed) {
|
|
2153
2190
|
// Repo-info slices (empty files, e.g. architecture) always run;
|
|
2154
2191
|
// file-backed slices run only when one of their files changed.
|
|
2155
2192
|
slices = slices.filter((s) => s.files.length === 0 || s.files.some((f) => changed.has(f)));
|
|
2156
2193
|
}
|
|
2157
2194
|
slicesByLens.set(lensId, slices);
|
|
2195
|
+
if (slices.length === 0) {
|
|
2196
|
+
const globs = lens.globsFor(info).filter(Boolean);
|
|
2197
|
+
skipReasons.set(lensId, globs.length === 0
|
|
2198
|
+
? "the lens has no file patterns for this language"
|
|
2199
|
+
: matchedBeforeIncremental > 0
|
|
2200
|
+
? "incremental: none of this lens's files changed since the previous run"
|
|
2201
|
+
: `no files matched ${globs.join(", ")}${lens.skipTestFiles ? " (test files excluded)" : ""}`);
|
|
2202
|
+
}
|
|
2158
2203
|
const lensModel = modelForLens(lensId);
|
|
2159
2204
|
const { pricing: lensPricing, outputCap: lensOutputCap } = resolved.get(lensModel);
|
|
2160
2205
|
const maxTokens = lensOutputCap ? Math.min(lens.maxTokens, lensOutputCap) : lens.maxTokens;
|
|
@@ -2263,7 +2308,15 @@ export async function runBroadsideSubmit(cwd, apiKey, opts = {}) {
|
|
|
2263
2308
|
if (requests.length === 0) {
|
|
2264
2309
|
// No files matched the lens's globs. That is a coverage gap to
|
|
2265
2310
|
// report, not a batch to submit — the API rejects empty batches.
|
|
2311
|
+
// Name the globs: a JavaScript service whose server lives at
|
|
2312
|
+
// src/server.js gets no security review (that lens reads server/**,
|
|
2313
|
+
// **/auth*, **/middleware/**), and "skipped (0 request(s))" alone
|
|
2314
|
+
// read as an empty repository rather than a lens that looked in
|
|
2315
|
+
// the wrong place.
|
|
2266
2316
|
entry.status = "skipped";
|
|
2317
|
+
const reason = skipReasons.get(lensId);
|
|
2318
|
+
if (reason)
|
|
2319
|
+
entry.reason = reason;
|
|
2267
2320
|
continue;
|
|
2268
2321
|
}
|
|
2269
2322
|
submissions.push((async () => {
|
|
@@ -2284,6 +2337,12 @@ export async function runBroadsideSubmit(cwd, apiKey, opts = {}) {
|
|
|
2284
2337
|
})());
|
|
2285
2338
|
}
|
|
2286
2339
|
await Promise.allSettled(submissions);
|
|
2340
|
+
// A run with no batch behind it has nothing in flight. Every lens was
|
|
2341
|
+
// skipped or refused, so no poll will ever complete it; leaving it
|
|
2342
|
+
// "in-flight" had status listing a refused run above the completed ones
|
|
2343
|
+
// with synthesis and triage "pending" forever.
|
|
2344
|
+
if (!Object.values(run.batches).some((entry) => entry.batchId))
|
|
2345
|
+
run.status = "failed";
|
|
2287
2346
|
await persistBroadsideRun(broadsideDir, run);
|
|
2288
2347
|
// What the provider just said about each model's batch endpoint outlives
|
|
2289
2348
|
// the run: the `models` action reads it back (#141).
|
|
@@ -2625,9 +2684,12 @@ export async function runBroadsideCollect(cwd, apiKey, opts = {}) {
|
|
|
2625
2684
|
}
|
|
2626
2685
|
await persistBroadsideRun(broadsideDir, run);
|
|
2627
2686
|
}
|
|
2628
|
-
// #133: re-submit truncated slices once with a bumped output cap
|
|
2629
|
-
// requests are pure, so re-running is always safe;
|
|
2630
|
-
// coverage the first pass lost to a max_tokens
|
|
2687
|
+
// #133: re-submit truncated slices once with a bumped output cap and low
|
|
2688
|
+
// reasoning effort. Batch requests are pure, so re-running is always safe;
|
|
2689
|
+
// the aim is to recover coverage the first pass lost to a max_tokens
|
|
2690
|
+
// cutoff, not to loop forever. Low effort because the cutoff is usually
|
|
2691
|
+
// thinking, and a doubled budget doubled the thinking where a token cap
|
|
2692
|
+
// was ignored (see retryReasoningFor).
|
|
2631
2693
|
//
|
|
2632
2694
|
// All bumped requests for one model go out as ONE batch, and the batches
|
|
2633
2695
|
// (one per model, since a batch carries a single model) are polled
|
|
@@ -2658,7 +2720,10 @@ export async function runBroadsideCollect(cwd, apiKey, opts = {}) {
|
|
|
2658
2720
|
if (bumpedMax <= previousMax)
|
|
2659
2721
|
continue; // already at the ceiling
|
|
2660
2722
|
const group = byModel.get(lensModel) ?? { requests: [], slices: new Map() };
|
|
2661
|
-
group.requests.push({
|
|
2723
|
+
group.requests.push({
|
|
2724
|
+
...original,
|
|
2725
|
+
body: { ...original.body, max_tokens: bumpedMax, reasoning: retryReasoningFor(original.body.reasoning) },
|
|
2726
|
+
});
|
|
2662
2727
|
group.slices.set(stored.customId, stored);
|
|
2663
2728
|
byModel.set(lensModel, group);
|
|
2664
2729
|
}
|
|
@@ -2959,7 +3024,7 @@ function parseSynthesisTopFindings(content) {
|
|
|
2959
3024
|
}
|
|
2960
3025
|
}
|
|
2961
3026
|
// ---------- formatting helpers for tool output ----------
|
|
2962
|
-
function describeIncrementalFallback(reason) {
|
|
3027
|
+
export function describeIncrementalFallback(reason) {
|
|
2963
3028
|
switch (reason) {
|
|
2964
3029
|
case "dirty-worktree":
|
|
2965
3030
|
return "the working tree has uncommitted changes, so there is no committed state to diff against";
|
|
@@ -2993,8 +3058,13 @@ export function estimateSubmitText(result, lenses) {
|
|
|
2993
3058
|
const override = entry.model ? ` on ${entry.model}` : "";
|
|
2994
3059
|
// A rejected lens says why: the message is the only way to tell a
|
|
2995
3060
|
// catalog id with no batch endpoint from a full job quota, and both
|
|
2996
|
-
// used to read as a bare "rejected".
|
|
2997
|
-
|
|
3061
|
+
// used to read as a bare "rejected". A skipped lens names the globs
|
|
3062
|
+
// that matched nothing.
|
|
3063
|
+
const reason = !entry.batchId && entry.error
|
|
3064
|
+
? ` — ${explainBatchError(entry.error)}`
|
|
3065
|
+
: !entry.batchId && entry.reason
|
|
3066
|
+
? ` — ${entry.reason}`
|
|
3067
|
+
: "";
|
|
2998
3068
|
lines.push(` ${lens.name}: ${status} (${entry.requests} request(s), ~$${entry.estimatedCost.toFixed(4)})${override}${reason}`);
|
|
2999
3069
|
}
|
|
3000
3070
|
if (result.repo) {
|
|
@@ -3178,6 +3248,22 @@ export function collectResultText(result) {
|
|
|
3178
3248
|
lines.push("", "Disclaimer: Broad-Side findings are unverified scouting signals from a batch model, not validated claims.");
|
|
3179
3249
|
return lines.join("\n");
|
|
3180
3250
|
}
|
|
3251
|
+
/**
|
|
3252
|
+
* An `onStatus` callback that appends one line to `lines` per *change* of a
|
|
3253
|
+
* lens's polled status. Every poll used to append a line, so a four-minute
|
|
3254
|
+
* wait returned twenty-six identical "in_progress (0/1)" lines per lens
|
|
3255
|
+
* before the result (0.22.0 live run).
|
|
3256
|
+
*/
|
|
3257
|
+
export function statusLineWriter(lines) {
|
|
3258
|
+
const last = new Map();
|
|
3259
|
+
return (lensId, status, counts) => {
|
|
3260
|
+
const line = ` ${lensId}: ${status} (${counts.completed ?? 0}/${counts.total ?? "?"})`;
|
|
3261
|
+
if (last.get(lensId) === line)
|
|
3262
|
+
return;
|
|
3263
|
+
last.set(lensId, line);
|
|
3264
|
+
lines.push(line);
|
|
3265
|
+
};
|
|
3266
|
+
}
|
|
3181
3267
|
export function statusText(state) {
|
|
3182
3268
|
if (state.runs.length === 0) {
|
|
3183
3269
|
return "No Broad-Side runs recorded. Call codecarto_broadside with action 'submit' first.";
|
|
@@ -3195,7 +3281,8 @@ export function statusText(state) {
|
|
|
3195
3281
|
const entry = run.batches[lensId];
|
|
3196
3282
|
if (!entry)
|
|
3197
3283
|
continue;
|
|
3198
|
-
lines.push(` ${lensId}: ${entry.status}${entry.batchId ? ` (${entry.batchId})` : ""}${entry.cost !== undefined ? `, $${entry.cost.toFixed(6)}` : ""}`
|
|
3284
|
+
lines.push(` ${lensId}: ${entry.status}${entry.batchId ? ` (${entry.batchId})` : ""}${entry.cost !== undefined ? `, $${entry.cost.toFixed(6)}` : ""}` +
|
|
3285
|
+
(entry.status === "skipped" && entry.reason ? ` — ${entry.reason}` : ""));
|
|
3199
3286
|
}
|
|
3200
3287
|
lines.push(` synthesis: ${run.synthesis.status}`);
|
|
3201
3288
|
lines.push(` triage: ${run.triage?.status ?? "pending"}`);
|
|
@@ -9,7 +9,7 @@ import { parseNextFlags } from "./next-flags.js";
|
|
|
9
9
|
import { buildPiGuideMessage } from "./guide-framing.js";
|
|
10
10
|
import { isCtxLive, notifyCtx } from "./notify.js";
|
|
11
11
|
import { phaseCompactionExtension } from "./phase-compaction.js";
|
|
12
|
-
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, 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";
|
|
12
|
+
import { applyAmendment, buildPhasePrompt, buildSkillPrompt, buildValidationSummary, backupWorkspaceState, canonicalPath, copyPackagedWorkspace, computePerPhaseTotals, computeTotals, ConfidentialityMismatchError, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, describeDanglingCarryForward, describeMissingCompletedOutputs, describeStuckPipeline, expandTilde, getPipelineLabel, getWorkspaceState, isWithinPath, resolveExistingPrefix, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, BroadsideConfigError, defaultBroadsideConfig, BroadsideCancelledError, broadsideDirFor, collectResultText, estimateSubmitText, getLens, listAmendmentNames, listBatchModels, listGuideTopics, listScaffoldRefreshFiles, listMissingCompletedOutputs, listSkillNames, resolvePipelineOutcome, resolveSkillName, loadAmendmentFile, loadBroadsideConfig, modelsText, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, statusText, describeConfigProblems, describeIncrementalFallback, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, readBroadsideSkill, readGuide, refreshScaffold, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, resolvePublishSourceRepo, SourceRepoMismatchError, runPhasePreflight, SCAFFOLD_REFRESH_PROTECTED, seedOrchestratorFiles, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, writeDashboard, } from "../../core/index.js";
|
|
13
13
|
import { initLibrary } from "../../core/library.js";
|
|
14
14
|
import { resolveUserConfigPath } from "../../core/orchestrator-config.js";
|
|
15
15
|
const STATUS_WIDGET_ID = "codecarto-widget";
|
|
@@ -144,11 +144,14 @@ function describeBroadsideEstimate(estimate) {
|
|
|
144
144
|
? `This EXCEEDS the configured max_cost of $${estimate.maxCost.toFixed(2)}. Approving here overrides it for this run.`
|
|
145
145
|
: `Within the configured max_cost of $${estimate.maxCost.toFixed(2)}.`);
|
|
146
146
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
147
|
+
// Only a requested incremental run has anything to say here. The dialog
|
|
148
|
+
// used to print "Incremental was requested but the tree is dirty" on every
|
|
149
|
+
// dirty tree, requested or not — a full scan that nobody asked to shrink
|
|
150
|
+
// read as a fallback.
|
|
151
|
+
if (estimate.incremental?.requested) {
|
|
152
|
+
lines.push(estimate.incremental.applied
|
|
153
|
+
? `Incremental: only modules changed since ${(estimate.baseHead ?? "").slice(0, 8)} are included.`
|
|
154
|
+
: `Incremental was requested but NOT applied — ${describeIncrementalFallback(estimate.incremental.reason)}. This is a full scan.`);
|
|
152
155
|
}
|
|
153
156
|
lines.push("", "The estimate is a pre-flight prediction from file sizes; OpenRouter bills actual usage.");
|
|
154
157
|
return lines.join("\n");
|
|
@@ -1114,6 +1117,9 @@ export default function codeCartographerExtension(pi) {
|
|
|
1114
1117
|
const lenses = flags.lenses.length > 0 ? flags.lenses : config.defaultLenses;
|
|
1115
1118
|
renderProgress("Slicing the repository and pricing the run…");
|
|
1116
1119
|
let submit;
|
|
1120
|
+
// Set when a headless run was refused over max_cost, so the cancel
|
|
1121
|
+
// message says so instead of reading as a user's "no".
|
|
1122
|
+
let headlessRefusal = null;
|
|
1117
1123
|
try {
|
|
1118
1124
|
submit = await runBroadsideSubmit(ctx.cwd, apiKey, {
|
|
1119
1125
|
lenses,
|
|
@@ -1128,14 +1134,32 @@ export default function codeCartographerExtension(pi) {
|
|
|
1128
1134
|
incremental: flags.incremental ?? config.incremental,
|
|
1129
1135
|
// Pi can ask, so it asks instead of refusing over max_cost the
|
|
1130
1136
|
// way MCP has to. An approval here IS the force flag.
|
|
1131
|
-
confirm: (estimate) =>
|
|
1137
|
+
confirm: (estimate) => {
|
|
1138
|
+
if (ctx.hasUI) {
|
|
1139
|
+
return ctx.ui.confirm(`Broad-Side will spend about $${estimate.totalCost.toFixed(4)}`, describeBroadsideEstimate(estimate));
|
|
1140
|
+
}
|
|
1141
|
+
// No dialog under `pi -p`: the stub answered "no" to every
|
|
1142
|
+
// estimate, so a headless submit could never fire. Behave as
|
|
1143
|
+
// the MCP surface does — an estimate within max_cost is
|
|
1144
|
+
// approved by the cap itself; one over it is refused, since
|
|
1145
|
+
// nobody is here to say yes — and print the breakdown either
|
|
1146
|
+
// way, because the dialog was the only place it showed.
|
|
1147
|
+
notifyCtx(ctx, describeBroadsideEstimate(estimate), "info");
|
|
1148
|
+
if (estimate.exceedsLimit) {
|
|
1149
|
+
headlessRefusal =
|
|
1150
|
+
`Broad-Side refused: the estimate ~$${estimate.totalCost.toFixed(4)} exceeds max_cost $${estimate.maxCost.toFixed(2)} ` +
|
|
1151
|
+
"and there is no dialog to approve it in a headless run. Raise --max-cost (0 for no limit) or run interactively.";
|
|
1152
|
+
return false;
|
|
1153
|
+
}
|
|
1154
|
+
return true;
|
|
1155
|
+
},
|
|
1132
1156
|
});
|
|
1133
1157
|
}
|
|
1134
1158
|
catch (error) {
|
|
1135
1159
|
if (ctx.hasUI)
|
|
1136
1160
|
ctx.ui.setWidget(BROADSIDE_WIDGET_ID, undefined);
|
|
1137
1161
|
if (error instanceof BroadsideCancelledError) {
|
|
1138
|
-
notifyCtx(ctx, "Broad-Side cancelled. Nothing was submitted.", "info");
|
|
1162
|
+
notifyCtx(ctx, headlessRefusal ?? "Broad-Side cancelled. Nothing was submitted.", headlessRefusal ? "error" : "info");
|
|
1139
1163
|
return;
|
|
1140
1164
|
}
|
|
1141
1165
|
notifyCtx(ctx, `Broad-Side submit failed: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
@@ -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, 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, 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";
|
|
@@ -1151,7 +1151,10 @@ export async function handleBroadside(args) {
|
|
|
1151
1151
|
includeSynthesis,
|
|
1152
1152
|
includeTriage,
|
|
1153
1153
|
retryTruncated,
|
|
1154
|
-
|
|
1154
|
+
// One line per *change* of a lens's status. Every poll used to
|
|
1155
|
+
// append a line, so a four-minute wait returned twenty-six
|
|
1156
|
+
// "in_progress (0/1)" lines before the result (0.22.0 live run).
|
|
1157
|
+
onStatus: statusLineWriter(lines),
|
|
1155
1158
|
}).catch((error) => {
|
|
1156
1159
|
// The `collect` action normalizes this same call; without it here,
|
|
1157
1160
|
// a failure during submit-with-wait reached the client as an
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codecartographer-pi",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.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",
|