codecartographer-pi 0.22.2 → 0.23.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 +15 -0
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/README.md +1 -1
- package/dist/core/broadside.d.ts +37 -0
- package/dist/core/broadside.js +70 -21
- package/dist/extensions/codecarto/completions.d.ts +18 -0
- package/dist/extensions/codecarto/completions.js +36 -0
- package/dist/extensions/codecarto/index.js +22 -41
- package/package.json +1 -1
|
@@ -86,6 +86,21 @@ Submits pre-flight the chosen model: pricing comes from the live catalog
|
|
|
86
86
|
model that does not advertise structured-output support is refused outright,
|
|
87
87
|
because every lens depends on `json_schema` response_format.
|
|
88
88
|
|
|
89
|
+
Each lens reads a scope of the repository. Architecture reads the manifest,
|
|
90
|
+
entry point, README, and file tree; defect, conventions, and porting read
|
|
91
|
+
every source file; the **security** and **API** lenses target where the
|
|
92
|
+
trust boundary usually lives — `server/**`, `**/auth*`, `**/middleware/**`,
|
|
93
|
+
`SECURITY.md` (security) and `server/**`, `api/**`, `src/server/**`,
|
|
94
|
+
`src/api/**`, `**/*routes*`, `**/*router*`, `**/*handler*`, `**/*endpoint*`
|
|
95
|
+
(API). A repository whose server is `src/server.js` matches none of those, so
|
|
96
|
+
when the targeted patterns find nothing those two lenses **fall back to every
|
|
97
|
+
source file** — priced as such, chunked at the lens's slice size rather than
|
|
98
|
+
truncated, and said so on the lens line of the estimate, the submit report,
|
|
99
|
+
`status`, and the prompt the model receives. A lens whose targeted patterns
|
|
100
|
+
and fallback both find nothing (only test files, say) is skipped with a line
|
|
101
|
+
naming both. `max_cost` is the guard against a fallback scan on a large
|
|
102
|
+
repository being more than you meant to spend.
|
|
103
|
+
|
|
89
104
|
Collect runs two cross-lens post-passes by default: **synthesis** (the
|
|
90
105
|
executive report) and **triage** (the prioritized work order). Pass
|
|
91
106
|
`include_synthesis: false` or `include_triage: false` on collect to skip one.
|
package/README.md
CHANGED
|
@@ -383,7 +383,7 @@ Each workflow tool accepts an absolute `cwd` for the target repository. `codecar
|
|
|
383
383
|
|
|
384
384
|
## Broad-Side (batch reconnaissance)
|
|
385
385
|
|
|
386
|
-
Broad-Side is the cheap sweep you run *before* the expensive interactive run. It fires six analysis lenses — architecture, API surface, security, mechanical defect scan, convention extraction, porting — at a repository as single-turn prompts over the [OpenRouter Batch API](https://openrouter.ai/docs), then cross-references them into one executive report (`synthesis.md`) and a prioritized P0–P3 work order (`triage.md`).
|
|
386
|
+
Broad-Side is the cheap sweep you run *before* the expensive interactive run. It fires six analysis lenses — architecture, API surface, security, mechanical defect scan, convention extraction, porting — at a repository as single-turn prompts over the [OpenRouter Batch API](https://openrouter.ai/docs), then cross-references them into one executive report (`synthesis.md`) and a prioritized P0–P3 work order (`triage.md`). The security and API lenses target the paths where a trust boundary usually lives (`server/`, `auth*`, `middleware/`, routers and handlers) and fall back to every source file when a repository has none of them — priced as such, and said so on the estimate — so a service whose server is `src/server.js` still gets its security review.
|
|
387
387
|
|
|
388
388
|
**Broad-Side findings are unverified scouting leads, not evidence.** Each lens is one shot: no cross-file traversal, no runtime verification, no builds, no tests. Every finding is a `file:line` pointer that the interactive pipeline — or you — must confirm before it is a fact. That division of labor is the point: a sub-dollar unattended sweep that tells the expensive run where to look. Nothing downstream may cite a Broad-Side report as a source.
|
|
389
389
|
|
package/dist/core/broadside.d.ts
CHANGED
|
@@ -121,6 +121,12 @@ export type FileSlice = {
|
|
|
121
121
|
redactedValues?: number;
|
|
122
122
|
/** The files in this slice that had at least one value redacted. */
|
|
123
123
|
redactedFiles?: string[];
|
|
124
|
+
/**
|
|
125
|
+
* Set when the lens's targeted globs matched nothing and the slice was
|
|
126
|
+
* built from its fallback globs instead (#319). The estimate, the batch
|
|
127
|
+
* entry, and the prompt all say so.
|
|
128
|
+
*/
|
|
129
|
+
fallback?: string;
|
|
124
130
|
};
|
|
125
131
|
/**
|
|
126
132
|
* OpenRouter's unified `reasoning` control, as sent on a lens request.
|
|
@@ -203,6 +209,12 @@ export type BroadsideBatchEntry = {
|
|
|
203
209
|
error?: unknown;
|
|
204
210
|
/** Why a `skipped` lens had nothing to submit: the globs that matched no file. */
|
|
205
211
|
reason?: string;
|
|
212
|
+
/**
|
|
213
|
+
* Set when the lens scanned its fallback scope because its targeted globs
|
|
214
|
+
* matched nothing (#319): "no files matched …; scanned all javascript
|
|
215
|
+
* sources instead". Absent for a targeted scan.
|
|
216
|
+
*/
|
|
217
|
+
fallback?: string;
|
|
206
218
|
/** Set when this lens used a model other than the run default. */
|
|
207
219
|
model?: string;
|
|
208
220
|
/** The completion ceiling of this lens's model; bounds the truncation retry. */
|
|
@@ -348,6 +360,8 @@ export type BroadsideEstimate = {
|
|
|
348
360
|
/** The model this lens would use — `model` unless a per-lens override applies. */
|
|
349
361
|
model: string;
|
|
350
362
|
pricing: ModelPricing;
|
|
363
|
+
/** Set when this lens is priced on its fallback scope (#319); see BroadsideBatchEntry.fallback. */
|
|
364
|
+
fallback?: string;
|
|
351
365
|
}>;
|
|
352
366
|
/** True when at least one lens uses a model other than the run default. */
|
|
353
367
|
mixedModels: boolean;
|
|
@@ -490,6 +504,15 @@ type LensDefinition = {
|
|
|
490
504
|
reasoning?: BroadsideReasoning;
|
|
491
505
|
skipTestFiles?: boolean;
|
|
492
506
|
globsFor: (info: RepoInfo) => string[];
|
|
507
|
+
/**
|
|
508
|
+
* Where to look when `globsFor` matches nothing (#319). The security and
|
|
509
|
+
* api lenses target server/, auth, and middleware paths because that is
|
|
510
|
+
* where the trust boundary usually lives; a service whose server is
|
|
511
|
+
* `src/server.js` matched none of them and got no security review at all.
|
|
512
|
+
* The fallback is the language's whole source set — priced as such, and
|
|
513
|
+
* said so in the estimate, the run record, and the prompt.
|
|
514
|
+
*/
|
|
515
|
+
fallbackGlobsFor?: (info: RepoInfo) => string[];
|
|
493
516
|
systemPrompt: (info: RepoInfo) => string;
|
|
494
517
|
userPrompt: (info: RepoInfo, source: string, moduleName: string) => string;
|
|
495
518
|
};
|
|
@@ -500,6 +523,20 @@ export declare const BROADSIDE_LANGUAGES: readonly ["go", "python", "rust", "typ
|
|
|
500
523
|
export declare function collectRepoInfo(targetDir: string, opts?: {
|
|
501
524
|
redact?: boolean;
|
|
502
525
|
}): Promise<RepoInfo>;
|
|
526
|
+
type CollectedFile = {
|
|
527
|
+
relPath: string;
|
|
528
|
+
moduleName: string;
|
|
529
|
+
};
|
|
530
|
+
/**
|
|
531
|
+
* The files a lens will read: its targeted globs, or — when those match
|
|
532
|
+
* nothing and the lens declares a fallback — the fallback globs, with a
|
|
533
|
+
* sentence saying so (#319). The sentence travels to the estimate, the
|
|
534
|
+
* batch entry, and the prompt, so a fallback scan is never a silent one.
|
|
535
|
+
*/
|
|
536
|
+
export declare function selectLensFiles(allFiles: string[], lens: LensDefinition, info: RepoInfo): {
|
|
537
|
+
files: CollectedFile[];
|
|
538
|
+
fallback?: string;
|
|
539
|
+
};
|
|
503
540
|
export declare function gatherSlices(targetDir: string, lens: LensDefinition, info: RepoInfo, opts?: {
|
|
504
541
|
redact?: boolean;
|
|
505
542
|
}): Promise<FileSlice[]>;
|
package/dist/core/broadside.js
CHANGED
|
@@ -698,6 +698,7 @@ const LENSES = {
|
|
|
698
698
|
"**/*handler*",
|
|
699
699
|
"**/*endpoint*",
|
|
700
700
|
],
|
|
701
|
+
fallbackGlobsFor: (info) => [info.sourceGlob],
|
|
701
702
|
systemPrompt: () => "You are a senior API auditor. Given source files from an HTTP server, " +
|
|
702
703
|
"extract every HTTP endpoint (method, path, handler function, auth requirement) " +
|
|
703
704
|
"and every key request/response data type. Return a JSON object following the " +
|
|
@@ -718,6 +719,7 @@ const LENSES = {
|
|
|
718
719
|
globsFor: (info) => info.language === "go"
|
|
719
720
|
? ["server/**/*.go", "server/*.go", "**/auth*.go", "**/middleware/**/*.go", "SECURITY.md"]
|
|
720
721
|
: ["server/**", "**/auth*", "**/middleware/**", "SECURITY.md"],
|
|
722
|
+
fallbackGlobsFor: (info) => [info.sourceGlob],
|
|
721
723
|
systemPrompt: () => "You are a security engineer performing a first-pass review of a codebase. " +
|
|
722
724
|
"Given source files, identify potential security issues — focusing on " +
|
|
723
725
|
"authentication, authorization, input validation, TLS, secrets handling, " +
|
|
@@ -1237,8 +1239,7 @@ function resolveSliceMode(lens, files, totalChars) {
|
|
|
1237
1239
|
return lens.sliceBy;
|
|
1238
1240
|
return totalChars > lens.maxChars ? "directory" : "none";
|
|
1239
1241
|
}
|
|
1240
|
-
function
|
|
1241
|
-
const globs = lens.globsFor(info).filter(Boolean);
|
|
1242
|
+
function collectFilesMatching(allFiles, lens, globs) {
|
|
1242
1243
|
if (globs.length === 0)
|
|
1243
1244
|
return [];
|
|
1244
1245
|
const out = [];
|
|
@@ -1253,6 +1254,30 @@ function collectLensFiles(allFiles, lens, info) {
|
|
|
1253
1254
|
}
|
|
1254
1255
|
return out;
|
|
1255
1256
|
}
|
|
1257
|
+
/**
|
|
1258
|
+
* The files a lens will read: its targeted globs, or — when those match
|
|
1259
|
+
* nothing and the lens declares a fallback — the fallback globs, with a
|
|
1260
|
+
* sentence saying so (#319). The sentence travels to the estimate, the
|
|
1261
|
+
* batch entry, and the prompt, so a fallback scan is never a silent one.
|
|
1262
|
+
*/
|
|
1263
|
+
export function selectLensFiles(allFiles, lens, info) {
|
|
1264
|
+
const globs = lens.globsFor(info).filter(Boolean);
|
|
1265
|
+
const targeted = collectFilesMatching(allFiles, lens, globs);
|
|
1266
|
+
if (targeted.length > 0 || globs.length === 0 || !lens.fallbackGlobsFor)
|
|
1267
|
+
return { files: targeted };
|
|
1268
|
+
const fallbackGlobs = lens.fallbackGlobsFor(info).filter(Boolean);
|
|
1269
|
+
const files = collectFilesMatching(allFiles, lens, fallbackGlobs);
|
|
1270
|
+
if (files.length === 0)
|
|
1271
|
+
return { files };
|
|
1272
|
+
return {
|
|
1273
|
+
files,
|
|
1274
|
+
fallback: `no files matched ${globs.join(", ")}${lens.skipTestFiles ? " (test files excluded)" : ""}; ` +
|
|
1275
|
+
`scanned all ${info.language} sources (${fallbackGlobs.join(", ")}) instead`,
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
function collectLensFiles(allFiles, lens, info) {
|
|
1279
|
+
return selectLensFiles(allFiles, lens, info).files;
|
|
1280
|
+
}
|
|
1256
1281
|
async function slurpFileList(targetDir, files, maxChars, redact = true) {
|
|
1257
1282
|
const slices = [];
|
|
1258
1283
|
let currentModule = "";
|
|
@@ -1329,16 +1354,18 @@ export async function gatherSlices(targetDir, lens, info, opts = {}) {
|
|
|
1329
1354
|
return [{ moduleName: "root", content: "", fileCount: 0, chars: 0, files: [] }];
|
|
1330
1355
|
}
|
|
1331
1356
|
const { files: allFiles } = await listRepoFiles(targetDir);
|
|
1332
|
-
const files =
|
|
1357
|
+
const { files, fallback } = selectLensFiles(allFiles, lens, info);
|
|
1333
1358
|
const totalChars = await sumFileSizes(targetDir, files);
|
|
1334
1359
|
const mode = resolveSliceMode(lens, files, totalChars);
|
|
1335
|
-
|
|
1360
|
+
const slices = mode === "none"
|
|
1336
1361
|
// Whole-repo slice: one module named after the repo, so a small
|
|
1337
1362
|
// repo produces a single request instead of one per directory.
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1363
|
+
? await slurpFileList(targetDir, files.map((f) => ({ ...f, moduleName: info.name })), lens.maxChars, redact)
|
|
1364
|
+
: await slurpFileList(targetDir, files, lens.maxChars, redact);
|
|
1365
|
+
if (fallback)
|
|
1366
|
+
for (const slice of slices)
|
|
1367
|
+
slice.fallback = fallback;
|
|
1368
|
+
return slices;
|
|
1342
1369
|
}
|
|
1343
1370
|
async function sumFileSizes(targetDir, files) {
|
|
1344
1371
|
let total = 0;
|
|
@@ -1362,7 +1389,17 @@ export function buildBatchRequest(lens, info, slice, index, sliceCount, model =
|
|
|
1362
1389
|
model,
|
|
1363
1390
|
messages: [
|
|
1364
1391
|
{ role: "system", content: lens.systemPrompt(info) },
|
|
1365
|
-
{
|
|
1392
|
+
{
|
|
1393
|
+
role: "user",
|
|
1394
|
+
content:
|
|
1395
|
+
// A fallback scan is not "server source files": say what it is,
|
|
1396
|
+
// so the model judges the trust boundary wherever it appears
|
|
1397
|
+
// and does not report the missing server/ as a finding (#319).
|
|
1398
|
+
(slice.fallback
|
|
1399
|
+
? `NOTE: this repository has no files under the paths this lens usually reads (${slice.fallback}). ` +
|
|
1400
|
+
"What follows is every source file it has; locate the trust boundary and the request-handling code wherever they live.\n\n"
|
|
1401
|
+
: "") + lens.userPrompt(info, slice.content, slice.moduleName),
|
|
1402
|
+
},
|
|
1366
1403
|
],
|
|
1367
1404
|
response_format: { type: "json_schema", json_schema: SCHEMAS[lens.schemaName] },
|
|
1368
1405
|
max_tokens: maxTokensOverride ?? lens.maxTokens,
|
|
@@ -2320,11 +2357,14 @@ export async function runBroadsideSubmit(cwd, apiKey, opts = {}) {
|
|
|
2320
2357
|
slicesByLens.set(lensId, slices);
|
|
2321
2358
|
if (slices.length === 0) {
|
|
2322
2359
|
const globs = lens.globsFor(info).filter(Boolean);
|
|
2360
|
+
const fallbackGlobs = lens.fallbackGlobsFor?.(info).filter(Boolean) ?? [];
|
|
2323
2361
|
skipReasons.set(lensId, globs.length === 0
|
|
2324
2362
|
? "the lens has no file patterns for this language"
|
|
2325
2363
|
: matchedBeforeIncremental > 0
|
|
2326
2364
|
? "incremental: none of this lens's files changed since the previous run"
|
|
2327
|
-
: `no files matched ${globs.join(", ")}
|
|
2365
|
+
: `no files matched ${globs.join(", ")}` +
|
|
2366
|
+
(fallbackGlobs.length > 0 ? ` or the fallback ${fallbackGlobs.join(", ")}` : "") +
|
|
2367
|
+
(lens.skipTestFiles ? " (test files excluded)" : ""));
|
|
2328
2368
|
}
|
|
2329
2369
|
const lensModel = modelForLens(lensId);
|
|
2330
2370
|
const { pricing: lensPricing, outputCap: lensOutputCap } = resolved.get(lensModel);
|
|
@@ -2347,15 +2387,19 @@ export async function runBroadsideSubmit(cwd, apiKey, opts = {}) {
|
|
|
2347
2387
|
const approved = await opts.confirm({
|
|
2348
2388
|
model,
|
|
2349
2389
|
pricing,
|
|
2350
|
-
lenses: perLensEstimate.map(({ lens, cost, maxTokens, lensModel, lensPricing }) =>
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2390
|
+
lenses: perLensEstimate.map(({ lens, cost, maxTokens, lensModel, lensPricing }) => {
|
|
2391
|
+
const fallback = (slicesByLens.get(lens.id) ?? []).find((slice) => slice.fallback)?.fallback;
|
|
2392
|
+
return {
|
|
2393
|
+
lensId: lens.id,
|
|
2394
|
+
name: lens.name,
|
|
2395
|
+
slices: (slicesByLens.get(lens.id) ?? []).length,
|
|
2396
|
+
maxTokens,
|
|
2397
|
+
cost,
|
|
2398
|
+
model: lensModel,
|
|
2399
|
+
pricing: lensPricing,
|
|
2400
|
+
...(fallback && { fallback }),
|
|
2401
|
+
};
|
|
2402
|
+
}),
|
|
2359
2403
|
mixedModels: perLensEstimate.some(({ lensModel }) => lensModel !== model),
|
|
2360
2404
|
totalCost: estimatedTotalCost,
|
|
2361
2405
|
inputTokens: estimatedInputTokens,
|
|
@@ -2419,12 +2463,15 @@ export async function runBroadsideSubmit(cwd, apiKey, opts = {}) {
|
|
|
2419
2463
|
const requests = slices.map((sl, i) => buildBatchRequest(lens, info, sl, i, slices.length, lensModel, maxTokens, config.reasoning ?? undefined));
|
|
2420
2464
|
for (const request of requests)
|
|
2421
2465
|
requestsByCustomId[request.custom_id] = request;
|
|
2466
|
+
const fallback = slices.find((slice) => slice.fallback)?.fallback;
|
|
2422
2467
|
const entry = {
|
|
2423
2468
|
batchId: "",
|
|
2424
2469
|
requests: requests.length,
|
|
2425
2470
|
status: "submitting",
|
|
2426
2471
|
submittedAt: new Date().toISOString(),
|
|
2427
2472
|
estimatedCost: priced.cost,
|
|
2473
|
+
// A scan of the fallback scope is recorded as such (#319).
|
|
2474
|
+
...(fallback && { fallback }),
|
|
2428
2475
|
// Recorded per lens so collect's truncation retry re-submits against
|
|
2429
2476
|
// the model and ceiling this lens actually used, not the run default.
|
|
2430
2477
|
...(lensModel !== model && { model: lensModel }),
|
|
@@ -3256,7 +3303,9 @@ export function estimateSubmitText(result, lenses) {
|
|
|
3256
3303
|
? ` — ${explainBatchError(entry.error)}`
|
|
3257
3304
|
: !entry.batchId && entry.reason
|
|
3258
3305
|
? ` — ${entry.reason}`
|
|
3259
|
-
:
|
|
3306
|
+
: entry.fallback
|
|
3307
|
+
? ` — ${entry.fallback}`
|
|
3308
|
+
: "";
|
|
3260
3309
|
lines.push(` ${lens.name}: ${status} (${entry.requests} request(s), ~$${entry.estimatedCost.toFixed(4)})${override}${reason}`);
|
|
3261
3310
|
}
|
|
3262
3311
|
if (result.repo) {
|
|
@@ -3492,7 +3541,7 @@ export function statusText(state) {
|
|
|
3492
3541
|
if (!entry)
|
|
3493
3542
|
continue;
|
|
3494
3543
|
lines.push(` ${lensId}: ${entry.status}${entry.batchId ? ` (${entry.batchId})` : ""}${entry.cost !== undefined ? `, $${entry.cost.toFixed(6)}` : ""}` +
|
|
3495
|
-
(entry.status === "skipped" && entry.reason ? ` — ${entry.reason}` : ""));
|
|
3544
|
+
(entry.status === "skipped" && entry.reason ? ` — ${entry.reason}` : entry.fallback ? ` — ${entry.fallback}` : ""));
|
|
3496
3545
|
}
|
|
3497
3546
|
lines.push(` synthesis: ${run.synthesis.status}`);
|
|
3498
3547
|
lines.push(` triage: ${run.triage?.status ?? "pending"}`);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface CompletionCandidate {
|
|
2
|
+
value: string;
|
|
3
|
+
label?: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface CompletionItem {
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Complete the last whitespace-separated token of `prefix` from `candidates`,
|
|
13
|
+
* returning items whose `value` is the full argument text Pi should put in
|
|
14
|
+
* the line — every earlier token, then the candidate. A candidate already
|
|
15
|
+
* typed as an earlier token is not offered again. Returns null when nothing
|
|
16
|
+
* matches, which is what Pi expects for "no popup".
|
|
17
|
+
*/
|
|
18
|
+
export declare function completeLastToken(prefix: string, candidates: readonly CompletionCandidate[]): CompletionItem[] | null;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Argument completion for the /codecarto-* slash commands.
|
|
2
|
+
//
|
|
3
|
+
// Pi hands `getArgumentCompletions` everything typed after the command name,
|
|
4
|
+
// and when the user accepts an item it replaces all of that text with the
|
|
5
|
+
// item's `value` (pi 0.85: `argumentText = textBeforeCursor.slice(spaceIndex
|
|
6
|
+
// + 1)` on the way in, `applyCompletion` slicing `cursorCol - prefix.length`
|
|
7
|
+
// on the way out). A completer that matches the *last* token but returns a
|
|
8
|
+
// bare flag therefore erases every flag typed before it: with the popup open,
|
|
9
|
+
// `/codecarto-next --auto --llm-steer` became `/codecarto-next --llm-steer` on
|
|
10
|
+
// Enter, the steered phase ran alone, and the auto run never started. The
|
|
11
|
+
// same shape turned `--auto --strict` into `--strict`, the one combination
|
|
12
|
+
// the parser rejects.
|
|
13
|
+
//
|
|
14
|
+
// So: match the last token, but return the whole line.
|
|
15
|
+
/**
|
|
16
|
+
* Complete the last whitespace-separated token of `prefix` from `candidates`,
|
|
17
|
+
* returning items whose `value` is the full argument text Pi should put in
|
|
18
|
+
* the line — every earlier token, then the candidate. A candidate already
|
|
19
|
+
* typed as an earlier token is not offered again. Returns null when nothing
|
|
20
|
+
* matches, which is what Pi expects for "no popup".
|
|
21
|
+
*/
|
|
22
|
+
export function completeLastToken(prefix, candidates) {
|
|
23
|
+
const tokens = prefix.split(/\s+/);
|
|
24
|
+
const last = tokens.pop() ?? "";
|
|
25
|
+
const earlier = tokens.filter((token) => token.length > 0);
|
|
26
|
+
const head = earlier.join(" ");
|
|
27
|
+
const typed = new Set(earlier);
|
|
28
|
+
const items = candidates
|
|
29
|
+
.filter((candidate) => candidate.value.startsWith(last) && !typed.has(candidate.value))
|
|
30
|
+
.map((candidate) => ({
|
|
31
|
+
value: head ? `${head} ${candidate.value}` : candidate.value,
|
|
32
|
+
label: candidate.label ?? candidate.value,
|
|
33
|
+
...(candidate.description && { description: candidate.description }),
|
|
34
|
+
}));
|
|
35
|
+
return items.length > 0 ? items : null;
|
|
36
|
+
}
|
|
@@ -6,6 +6,7 @@ import { parseDashboardFlags } from "./dashboard-flags.js";
|
|
|
6
6
|
import { narrateDashboard } from "./dashboard-narrator.js";
|
|
7
7
|
import { parseBroadsideFlags, KNOWN_BROADSIDE_TOKENS } from "./broadside-flags.js";
|
|
8
8
|
import { parseNextFlags } from "./next-flags.js";
|
|
9
|
+
import { completeLastToken } from "./completions.js";
|
|
9
10
|
import { buildPiGuideMessage } from "./guide-framing.js";
|
|
10
11
|
import { isCtxLive, notifyCtx } from "./notify.js";
|
|
11
12
|
import { phaseCompactionExtension } from "./phase-compaction.js";
|
|
@@ -129,11 +130,14 @@ function describeBroadsideEstimate(estimate) {
|
|
|
129
130
|
`Rates: $${estimate.pricing.inputPerM.toFixed(4)}/M in · $${estimate.pricing.outputPerM.toFixed(4)}/M out`,
|
|
130
131
|
"",
|
|
131
132
|
"Per lens:",
|
|
132
|
-
...estimate.lenses.map(({ name, slices, cost, model }) => {
|
|
133
|
+
...estimate.lenses.map(({ name, slices, cost, model, fallback }) => {
|
|
133
134
|
// Naming the model only when it differs keeps the common case quiet
|
|
134
135
|
// and makes a mixed-model run impossible to approve without noticing.
|
|
135
136
|
const override = estimate.mixedModels && model !== estimate.model ? ` on ${model}` : "";
|
|
136
|
-
|
|
137
|
+
// A lens priced on its fallback scope says so here, where the
|
|
138
|
+
// spend is approved — every source file, not a server directory (#319).
|
|
139
|
+
const scope = fallback ? `\n ↳ ${fallback}` : "";
|
|
140
|
+
return ` ${name}: ${slices} slice${slices === 1 ? "" : "s"} — ~$${cost.toFixed(4)}${override}${scope}`;
|
|
137
141
|
}),
|
|
138
142
|
"",
|
|
139
143
|
`Estimated total: ~$${estimate.totalCost.toFixed(4)} ` +
|
|
@@ -432,12 +436,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
432
436
|
});
|
|
433
437
|
pi.registerCommand("codecarto-init", {
|
|
434
438
|
description: "Initialize .codecarto/ in the current repository",
|
|
435
|
-
getArgumentCompletions: (prefix) => {
|
|
436
|
-
const items = Object.keys(PIPELINE_ALIASES)
|
|
437
|
-
.filter((value) => value.startsWith(prefix))
|
|
438
|
-
.map((value) => ({ value, label: value }));
|
|
439
|
-
return items.length > 0 ? items : null;
|
|
440
|
-
},
|
|
439
|
+
getArgumentCompletions: (prefix) => completeLastToken(prefix, Object.keys(PIPELINE_ALIASES).map((value) => ({ value }))),
|
|
441
440
|
handler: async (args, ctx) => {
|
|
442
441
|
const trimmedArgs = args.trim();
|
|
443
442
|
const pipelineChoice = resolvePipelineChoice(trimmedArgs);
|
|
@@ -536,12 +535,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
536
535
|
});
|
|
537
536
|
pi.registerCommand("codecarto-switch-pipeline", {
|
|
538
537
|
description: "Switch the active pipeline without losing findings or progress: /codecarto-switch-pipeline <variant>",
|
|
539
|
-
getArgumentCompletions: (prefix) => {
|
|
540
|
-
const items = Object.keys(PIPELINE_ALIASES)
|
|
541
|
-
.filter((value) => value.startsWith(prefix))
|
|
542
|
-
.map((value) => ({ value, label: value }));
|
|
543
|
-
return items.length > 0 ? items : null;
|
|
544
|
-
},
|
|
538
|
+
getArgumentCompletions: (prefix) => completeLastToken(prefix, Object.keys(PIPELINE_ALIASES).map((value) => ({ value }))),
|
|
545
539
|
handler: async (args, ctx) => {
|
|
546
540
|
const trimmedArgs = args.trim();
|
|
547
541
|
if (!trimmedArgs) {
|
|
@@ -595,16 +589,17 @@ export default function codeCartographerExtension(pi) {
|
|
|
595
589
|
// --strict is offered only once --auto is present, because on its own
|
|
596
590
|
// it is rejected — suggesting it standalone invites the one error the
|
|
597
591
|
// parser has.
|
|
592
|
+
// Each item's value is the whole argument line (see completions.ts):
|
|
593
|
+
// accepting `--llm-steer` after `--auto ` keeps the `--auto`.
|
|
598
594
|
const autoAlreadyTyped = prefix.includes("--auto");
|
|
599
|
-
|
|
600
|
-
{ value: "--auto",
|
|
601
|
-
{ value: "--llm-steer",
|
|
602
|
-
{ value: "--no-llm-steer",
|
|
595
|
+
return completeLastToken(prefix, [
|
|
596
|
+
{ value: "--auto", description: "run every remaining phase back to back (recommended with --llm-steer)" },
|
|
597
|
+
{ value: "--llm-steer", description: "seed each phase from the previous phase's closeout; no effect on the first phase" },
|
|
598
|
+
{ value: "--no-llm-steer", description: "force steering off when the workspace config turns it on" },
|
|
603
599
|
...(autoAlreadyTyped
|
|
604
|
-
? [{ value: "--strict",
|
|
600
|
+
? [{ value: "--strict", description: "with --auto: stop on PASS WITH GAPS instead of advancing" }]
|
|
605
601
|
: []),
|
|
606
|
-
]
|
|
607
|
-
return items.length > 0 ? items : null;
|
|
602
|
+
]);
|
|
608
603
|
},
|
|
609
604
|
handler: async (args, ctx) => {
|
|
610
605
|
const flags = parseNextFlags(args);
|
|
@@ -967,10 +962,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
967
962
|
description: "Read the packaged CodeCartographer agent guide into the session: /codecarto-guide [topic]",
|
|
968
963
|
getArgumentCompletions: async (prefix) => {
|
|
969
964
|
const topics = await listGuideTopics().catch(() => ["overview"]);
|
|
970
|
-
|
|
971
|
-
.filter((value) => value.startsWith(prefix))
|
|
972
|
-
.map((value) => ({ value, label: value }));
|
|
973
|
-
return items.length > 0 ? items : null;
|
|
965
|
+
return completeLastToken(prefix, topics.map((value) => ({ value })));
|
|
974
966
|
},
|
|
975
967
|
handler: async (args, ctx) => {
|
|
976
968
|
// The guide is packaged with the extension, not copied into a
|
|
@@ -1004,12 +996,9 @@ export default function codeCartographerExtension(pi) {
|
|
|
1004
996
|
});
|
|
1005
997
|
pi.registerCommand("codecarto-broadside", {
|
|
1006
998
|
description: "Batch reconnaissance (Broad-Side): /codecarto-broadside [submit|collect|status|models] [lenses…] [--model=ID] [--lens-model=LENS:ID] [flags]",
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
.map((value) => ({ value, label: value }));
|
|
1011
|
-
return items.length > 0 ? items : null;
|
|
1012
|
-
},
|
|
999
|
+
// Completes the token under the cursor, so lens names and flags are
|
|
1000
|
+
// offered after the action too, and keeps everything typed before it.
|
|
1001
|
+
getArgumentCompletions: (prefix) => completeLastToken(prefix, KNOWN_BROADSIDE_TOKENS.map((value) => ({ value }))),
|
|
1013
1002
|
handler: async (args, ctx) => {
|
|
1014
1003
|
const flags = parseBroadsideFlags(args);
|
|
1015
1004
|
if (flags.unknown.length > 0) {
|
|
@@ -1439,12 +1428,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
1439
1428
|
});
|
|
1440
1429
|
pi.registerCommand("codecarto-dashboard", {
|
|
1441
1430
|
description: "Regenerate .codecarto/dashboard.html (use --narrate for an LLM executive summary)",
|
|
1442
|
-
getArgumentCompletions: (prefix) => {
|
|
1443
|
-
const items = ["--narrate"]
|
|
1444
|
-
.filter((value) => value.startsWith(prefix))
|
|
1445
|
-
.map((value) => ({ value, label: value }));
|
|
1446
|
-
return items.length > 0 ? items : null;
|
|
1447
|
-
},
|
|
1431
|
+
getArgumentCompletions: (prefix) => completeLastToken(prefix, [{ value: "--narrate" }]),
|
|
1448
1432
|
handler: async (args, ctx) => {
|
|
1449
1433
|
const flags = parseDashboardFlags(args);
|
|
1450
1434
|
if (flags.unknown.length > 0) {
|
|
@@ -1515,10 +1499,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
1515
1499
|
description: "Apply a post-pipeline amendment from .codecarto/scratch/amendments/, after a preview: /codecarto-amend <name | scratch/amendments/name.yaml>",
|
|
1516
1500
|
getArgumentCompletions: async (prefix) => {
|
|
1517
1501
|
const names = await listAmendmentNames(join(sessionCwd ?? process.cwd(), ".codecarto"));
|
|
1518
|
-
|
|
1519
|
-
.filter((value) => value.startsWith(prefix))
|
|
1520
|
-
.map((value) => ({ value, label: value }));
|
|
1521
|
-
return items.length > 0 ? items : null;
|
|
1502
|
+
return completeLastToken(prefix, names.map((value) => ({ value })));
|
|
1522
1503
|
},
|
|
1523
1504
|
handler: async (args, ctx) => {
|
|
1524
1505
|
const state = await ensureWorkspaceState(ctx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codecartographer-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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",
|