squad-kit 0.8.0 → 0.9.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/CHANGELOG.md +24 -0
- package/dist/cli.js +2288 -1140
- package/dist/cli.js.map +1 -1
- package/dist/console-ui/assets/{index-DGzIogF0.js → index-Dnb4Fisq.js} +31 -31
- package/dist/console-ui/index.html +1 -1
- package/package.json +8 -1
- package/templates/prompts/generate-plan.md +78 -50
- package/templates/prompts/scout.md +42 -0
package/dist/cli.js
CHANGED
|
@@ -163,8 +163,8 @@ ${body}
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
// src/ui/error.ts
|
|
166
|
-
function renderError(
|
|
167
|
-
const msg =
|
|
166
|
+
function renderError(err2) {
|
|
167
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
168
168
|
process.stderr.write(` ${danger("\u2717")} ${msg}
|
|
169
169
|
`);
|
|
170
170
|
}
|
|
@@ -178,8 +178,8 @@ var SquadExit = class extends Error {
|
|
|
178
178
|
exitCode;
|
|
179
179
|
name = "SquadExit";
|
|
180
180
|
};
|
|
181
|
-
function isSquadExit(
|
|
182
|
-
return
|
|
181
|
+
function isSquadExit(err2) {
|
|
182
|
+
return err2 instanceof SquadExit;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
// src/commands/init.ts
|
|
@@ -303,15 +303,15 @@ function clampPlannerMaxOut(raw) {
|
|
|
303
303
|
Math.max(MIN_PLANNER_MAX_OUTPUT_TOKENS, Math.round(n))
|
|
304
304
|
);
|
|
305
305
|
}
|
|
306
|
-
function rejectSecretsInYaml(node, configFile,
|
|
306
|
+
function rejectSecretsInYaml(node, configFile, path38 = []) {
|
|
307
307
|
if (node === null || typeof node !== "object") return;
|
|
308
308
|
for (const [key, value] of Object.entries(node)) {
|
|
309
309
|
if (FORBIDDEN_KEYS.includes(key.toLowerCase())) {
|
|
310
310
|
throw new Error(
|
|
311
|
-
`Refusing to load ${configFile}: key "${[...
|
|
311
|
+
`Refusing to load ${configFile}: key "${[...path38, key].join(".")}" looks like a secret. Run \`squad config set planner\` or \`squad config set tracker\` to save it to .squad/secrets.yaml, then remove the key from config.yaml. Provider keys also work via ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY.`
|
|
312
312
|
);
|
|
313
313
|
}
|
|
314
|
-
if (typeof value === "object") rejectSecretsInYaml(value, configFile, [...
|
|
314
|
+
if (typeof value === "object") rejectSecretsInYaml(value, configFile, [...path38, key]);
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
317
|
function mergePlanner(base, override) {
|
|
@@ -323,8 +323,7 @@ function mergePlanner(base, override) {
|
|
|
323
323
|
budget: {
|
|
324
324
|
maxFileReads: override.budget?.maxFileReads ?? base?.budget?.maxFileReads ?? 25,
|
|
325
325
|
maxContextBytes: override.budget?.maxContextBytes ?? base?.budget?.maxContextBytes ?? 5e4,
|
|
326
|
-
maxDurationSeconds: override.budget?.maxDurationSeconds ?? base?.budget?.maxDurationSeconds ?? 180
|
|
327
|
-
maxCostUsd: override.budget?.maxCostUsd ?? base?.budget?.maxCostUsd
|
|
326
|
+
maxDurationSeconds: override.budget?.maxDurationSeconds ?? base?.budget?.maxDurationSeconds ?? 180
|
|
328
327
|
},
|
|
329
328
|
modelOverride: {
|
|
330
329
|
...base?.modelOverride ?? {},
|
|
@@ -333,7 +332,24 @@ function mergePlanner(base, override) {
|
|
|
333
332
|
cache: {
|
|
334
333
|
enabled: override.cache?.enabled ?? base?.cache?.enabled ?? true
|
|
335
334
|
},
|
|
336
|
-
maxOutputTokens: clampPlannerMaxOut(override.maxOutputTokens ?? base?.maxOutputTokens)
|
|
335
|
+
maxOutputTokens: clampPlannerMaxOut(override.maxOutputTokens ?? base?.maxOutputTokens),
|
|
336
|
+
stages: {
|
|
337
|
+
scout: {
|
|
338
|
+
enabled: override.stages?.scout?.enabled ?? base?.stages?.scout?.enabled ?? true,
|
|
339
|
+
modelOverride: override.stages?.scout?.modelOverride ?? base?.stages?.scout?.modelOverride,
|
|
340
|
+
maxFiles: override.stages?.scout?.maxFiles ?? base?.stages?.scout?.maxFiles ?? 12,
|
|
341
|
+
maxOutputTokens: override.stages?.scout?.maxOutputTokens ?? base?.stages?.scout?.maxOutputTokens ?? 2048
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
tools: {
|
|
345
|
+
grep: override.tools?.grep ?? base?.tools?.grep ?? true,
|
|
346
|
+
listDir: override.tools?.listDir ?? base?.tools?.listDir ?? true,
|
|
347
|
+
rangedRead: override.tools?.rangedRead ?? base?.tools?.rangedRead ?? true
|
|
348
|
+
},
|
|
349
|
+
validation: {
|
|
350
|
+
enabled: override.validation?.enabled ?? base?.validation?.enabled ?? true,
|
|
351
|
+
strict: override.validation?.strict ?? base?.validation?.strict ?? false
|
|
352
|
+
}
|
|
337
353
|
};
|
|
338
354
|
if (merged.modelOverride) {
|
|
339
355
|
const pruned = Object.fromEntries(
|
|
@@ -363,9 +379,9 @@ function parseConfig(raw, configFile) {
|
|
|
363
379
|
let parsed;
|
|
364
380
|
try {
|
|
365
381
|
parsed = yaml.load(raw);
|
|
366
|
-
} catch (
|
|
382
|
+
} catch (err2) {
|
|
367
383
|
throw new Error(
|
|
368
|
-
`Invalid YAML in ${configFile}: ${
|
|
384
|
+
`Invalid YAML in ${configFile}: ${err2.message}. Run \`squad doctor\` to review, or fix the file, or \`squad init --force\` to replace it.`
|
|
369
385
|
);
|
|
370
386
|
}
|
|
371
387
|
if (!parsed || typeof parsed !== "object") {
|
|
@@ -450,17 +466,17 @@ function loadSecrets(secretsFile) {
|
|
|
450
466
|
let raw;
|
|
451
467
|
try {
|
|
452
468
|
raw = fs4.readFileSync(secretsFile, "utf8");
|
|
453
|
-
} catch (
|
|
469
|
+
} catch (err2) {
|
|
454
470
|
throw new Error(
|
|
455
|
-
`Failed to read ${secretsFile}: ${
|
|
471
|
+
`Failed to read ${secretsFile}: ${err2.message}. Run \`squad doctor\` to diagnose, or delete the file and run \`squad config set planner\` or \`squad config set tracker\` to re-enter credentials.`
|
|
456
472
|
);
|
|
457
473
|
}
|
|
458
474
|
let parsed;
|
|
459
475
|
try {
|
|
460
476
|
parsed = yaml2.load(raw);
|
|
461
|
-
} catch (
|
|
477
|
+
} catch (err2) {
|
|
462
478
|
throw new Error(
|
|
463
|
-
`Invalid YAML in ${secretsFile}: ${
|
|
479
|
+
`Invalid YAML in ${secretsFile}: ${err2.message}. Run \`squad doctor\` to review paths and permissions, or fix the file, or delete it and run \`squad config set planner\` / \`squad config set tracker\` to re-enter secrets.`
|
|
464
480
|
);
|
|
465
481
|
}
|
|
466
482
|
if (parsed === null || parsed === void 0) return EMPTY;
|
|
@@ -515,21 +531,25 @@ function filterEmpty(obj) {
|
|
|
515
531
|
var PLANNER_MODEL_MAP = {
|
|
516
532
|
anthropic: {
|
|
517
533
|
plan: "claude-opus-4-7",
|
|
518
|
-
execute: "claude-haiku-4-5-20251001"
|
|
534
|
+
execute: "claude-haiku-4-5-20251001",
|
|
535
|
+
scout: "claude-haiku-4-5-20251001"
|
|
519
536
|
},
|
|
520
537
|
openai: {
|
|
521
538
|
// Unverified for 0.2.0. 0.2.1 will pin to dated snapshots once a maintainer can probe.
|
|
522
539
|
plan: "gpt-5.3-thinking",
|
|
523
|
-
execute: "gpt-5.3-mini"
|
|
540
|
+
execute: "gpt-5.3-mini",
|
|
541
|
+
scout: "gpt-5.3-mini"
|
|
524
542
|
},
|
|
525
543
|
google: {
|
|
526
544
|
// Unverified for 0.2.0. 0.2.1 will pin to dated snapshots once a maintainer can probe.
|
|
527
545
|
plan: "gemini-3-pro-latest",
|
|
528
|
-
execute: "gemini-3-flash-latest"
|
|
546
|
+
execute: "gemini-3-flash-latest",
|
|
547
|
+
scout: "gemini-3-flash-latest"
|
|
529
548
|
}
|
|
530
549
|
};
|
|
531
|
-
function modelFor(provider, phase, override) {
|
|
550
|
+
function modelFor(provider, phase, override, scoutModelId) {
|
|
532
551
|
if (phase === "plan" && override?.[provider]) return override[provider];
|
|
552
|
+
if (phase === "scout" && scoutModelId?.trim()) return scoutModelId.trim();
|
|
533
553
|
return PLANNER_MODEL_MAP[provider][phase];
|
|
534
554
|
}
|
|
535
555
|
function providerEnvVar(provider) {
|
|
@@ -850,8 +870,8 @@ async function probeJiraConnectivity(secrets, config2) {
|
|
|
850
870
|
});
|
|
851
871
|
if (res.ok) return { ok: true };
|
|
852
872
|
return { ok: false, status: res.status, detail: (await res.text()).slice(0, 200) };
|
|
853
|
-
} catch (
|
|
854
|
-
return { ok: false, detail:
|
|
873
|
+
} catch (err2) {
|
|
874
|
+
return { ok: false, detail: err2.message };
|
|
855
875
|
}
|
|
856
876
|
}
|
|
857
877
|
async function probeAzureConnectivity(secrets, config2) {
|
|
@@ -864,8 +884,8 @@ async function probeAzureConnectivity(secrets, config2) {
|
|
|
864
884
|
const res = await fetch(url, { headers: { Authorization: `Basic ${auth}` } });
|
|
865
885
|
if (res.ok) return { ok: true };
|
|
866
886
|
return { ok: false, status: res.status, detail: (await res.text()).slice(0, 200) };
|
|
867
|
-
} catch (
|
|
868
|
-
return { ok: false, detail:
|
|
887
|
+
} catch (err2) {
|
|
888
|
+
return { ok: false, detail: err2.message };
|
|
869
889
|
}
|
|
870
890
|
}
|
|
871
891
|
async function probeGitHubConnectivity(secrets, _config) {
|
|
@@ -885,8 +905,8 @@ async function probeGitHubConnectivity(secrets, _config) {
|
|
|
885
905
|
});
|
|
886
906
|
if (res.ok) return { ok: true };
|
|
887
907
|
return { ok: false, status: res.status, detail: (await res.text()).slice(0, 200) };
|
|
888
|
-
} catch (
|
|
889
|
-
return { ok: false, detail:
|
|
908
|
+
} catch (err2) {
|
|
909
|
+
return { ok: false, detail: err2.message };
|
|
890
910
|
}
|
|
891
911
|
}
|
|
892
912
|
|
|
@@ -1211,12 +1231,12 @@ async function one(ref, targetDir, headers, maxBytes, maxMb) {
|
|
|
1211
1231
|
let res;
|
|
1212
1232
|
try {
|
|
1213
1233
|
res = await fetch(ref.url, { headers });
|
|
1214
|
-
} catch (
|
|
1234
|
+
} catch (err2) {
|
|
1215
1235
|
return {
|
|
1216
1236
|
source: ref,
|
|
1217
1237
|
outcome: "skipped-error",
|
|
1218
1238
|
bytesWritten: 0,
|
|
1219
|
-
skipReason: `network error: ${
|
|
1239
|
+
skipReason: `network error: ${err2.message}`
|
|
1220
1240
|
};
|
|
1221
1241
|
}
|
|
1222
1242
|
if (!res.ok) {
|
|
@@ -1318,8 +1338,8 @@ var AzureDevOpsClient = class {
|
|
|
1318
1338
|
headers: { ...this.headers(), "content-type": "application/json" },
|
|
1319
1339
|
body: JSON.stringify({ query: `${wiql}` })
|
|
1320
1340
|
});
|
|
1321
|
-
} catch (
|
|
1322
|
-
throw new TrackerError(`Azure DevOps search failed: ${
|
|
1341
|
+
} catch (err2) {
|
|
1342
|
+
throw new TrackerError(`Azure DevOps search failed: ${err2.message}`, "network");
|
|
1323
1343
|
}
|
|
1324
1344
|
if (!wiqlRes.ok) throw this.mapHttpError(wiqlRes.status, "search");
|
|
1325
1345
|
const wiqlBody = await wiqlRes.json();
|
|
@@ -1329,8 +1349,8 @@ var AzureDevOpsClient = class {
|
|
|
1329
1349
|
let batchRes;
|
|
1330
1350
|
try {
|
|
1331
1351
|
batchRes = await fetch(batchUrl, { headers: this.headers() });
|
|
1332
|
-
} catch (
|
|
1333
|
-
throw new TrackerError(`Azure DevOps work items fetch failed: ${
|
|
1352
|
+
} catch (err2) {
|
|
1353
|
+
throw new TrackerError(`Azure DevOps work items fetch failed: ${err2.message}`, "network");
|
|
1334
1354
|
}
|
|
1335
1355
|
if (!batchRes.ok) throw this.mapHttpError(batchRes.status, "search");
|
|
1336
1356
|
const batchBody = await batchRes.json();
|
|
@@ -1352,8 +1372,8 @@ var AzureDevOpsClient = class {
|
|
|
1352
1372
|
let res;
|
|
1353
1373
|
try {
|
|
1354
1374
|
res = await fetch(url, { headers: this.headers() });
|
|
1355
|
-
} catch (
|
|
1356
|
-
throw new TrackerError(`Azure DevOps fetch failed: ${
|
|
1375
|
+
} catch (err2) {
|
|
1376
|
+
throw new TrackerError(`Azure DevOps fetch failed: ${err2.message}`, "network");
|
|
1357
1377
|
}
|
|
1358
1378
|
if (!res.ok) throw this.mapHttpError(res.status, id);
|
|
1359
1379
|
const body = await res.json();
|
|
@@ -1459,8 +1479,8 @@ var GitHubClient = class {
|
|
|
1459
1479
|
let res;
|
|
1460
1480
|
try {
|
|
1461
1481
|
res = await fetch(url, { headers: this.headers() });
|
|
1462
|
-
} catch (
|
|
1463
|
-
throw new TrackerError(`GitHub fetch failed: ${
|
|
1482
|
+
} catch (err2) {
|
|
1483
|
+
throw new TrackerError(`GitHub fetch failed: ${err2.message}`, "network");
|
|
1464
1484
|
}
|
|
1465
1485
|
if (!res.ok) throw this.mapHttpError(res, number);
|
|
1466
1486
|
const body = await res.json();
|
|
@@ -1475,8 +1495,8 @@ var GitHubClient = class {
|
|
|
1475
1495
|
let res2;
|
|
1476
1496
|
try {
|
|
1477
1497
|
res2 = await fetch(url2, { headers: this.headers() });
|
|
1478
|
-
} catch (
|
|
1479
|
-
throw new TrackerError(`GitHub search failed: ${
|
|
1498
|
+
} catch (err2) {
|
|
1499
|
+
throw new TrackerError(`GitHub search failed: ${err2.message}`, "network");
|
|
1480
1500
|
}
|
|
1481
1501
|
if (!res2.ok) throw this.mapHttpError(res2, "search");
|
|
1482
1502
|
const body2 = await res2.json();
|
|
@@ -1494,9 +1514,9 @@ var GitHubClient = class {
|
|
|
1494
1514
|
url: issue.url
|
|
1495
1515
|
}
|
|
1496
1516
|
];
|
|
1497
|
-
} catch (
|
|
1498
|
-
if (
|
|
1499
|
-
throw
|
|
1517
|
+
} catch (err2) {
|
|
1518
|
+
if (err2 instanceof TrackerError && err2.kind === "not-found") return [];
|
|
1519
|
+
throw err2;
|
|
1500
1520
|
}
|
|
1501
1521
|
}
|
|
1502
1522
|
const searchQuery = `${q} repo:${this.cfg.owner}/${this.cfg.repo}`;
|
|
@@ -1504,8 +1524,8 @@ var GitHubClient = class {
|
|
|
1504
1524
|
let res;
|
|
1505
1525
|
try {
|
|
1506
1526
|
res = await fetch(url, { headers: this.headers() });
|
|
1507
|
-
} catch (
|
|
1508
|
-
throw new TrackerError(`GitHub search failed: ${
|
|
1527
|
+
} catch (err2) {
|
|
1528
|
+
throw new TrackerError(`GitHub search failed: ${err2.message}`, "network");
|
|
1509
1529
|
}
|
|
1510
1530
|
if (!res.ok) throw this.mapHttpError(res, "search");
|
|
1511
1531
|
const body = await res.json();
|
|
@@ -1645,10 +1665,13 @@ var JiraClient = class {
|
|
|
1645
1665
|
let res;
|
|
1646
1666
|
try {
|
|
1647
1667
|
res = await fetch(url, { headers: this.headers() });
|
|
1648
|
-
} catch (
|
|
1649
|
-
throw new TrackerError(`Jira fetch failed: ${
|
|
1668
|
+
} catch (err2) {
|
|
1669
|
+
throw new TrackerError(`Jira fetch failed: ${err2.message}`, "network");
|
|
1670
|
+
}
|
|
1671
|
+
if (!res.ok) {
|
|
1672
|
+
const errBody = await res.text().catch(() => "");
|
|
1673
|
+
throw this.mapHttpError(res.status, id, errBody);
|
|
1650
1674
|
}
|
|
1651
|
-
if (!res.ok) throw this.mapHttpError(res.status, id);
|
|
1652
1675
|
const body = await res.json();
|
|
1653
1676
|
const fields = body.fields ?? {};
|
|
1654
1677
|
const rendered = body.renderedFields ?? {};
|
|
@@ -1680,7 +1703,7 @@ var JiraClient = class {
|
|
|
1680
1703
|
if (/^[A-Z][A-Z0-9_]+-\d+$/i.test(q)) {
|
|
1681
1704
|
jql = `issuekey = "${q.toUpperCase()}"`;
|
|
1682
1705
|
} else if (q.length === 0) {
|
|
1683
|
-
jql = "
|
|
1706
|
+
jql = "updated >= -90d ORDER BY updated DESC";
|
|
1684
1707
|
} else {
|
|
1685
1708
|
const esc = q.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1686
1709
|
jql = `text ~ "${esc}" order by updated DESC`;
|
|
@@ -1689,10 +1712,13 @@ var JiraClient = class {
|
|
|
1689
1712
|
let res;
|
|
1690
1713
|
try {
|
|
1691
1714
|
res = await fetch(url, { headers: this.headers() });
|
|
1692
|
-
} catch (
|
|
1693
|
-
throw new TrackerError(`Jira search failed: ${
|
|
1715
|
+
} catch (err2) {
|
|
1716
|
+
throw new TrackerError(`Jira search failed: ${err2.message}`, "network");
|
|
1717
|
+
}
|
|
1718
|
+
if (!res.ok) {
|
|
1719
|
+
const errBody = await res.text().catch(() => "");
|
|
1720
|
+
throw this.mapHttpError(res.status, "search", errBody);
|
|
1694
1721
|
}
|
|
1695
|
-
if (!res.ok) throw this.mapHttpError(res.status, "search");
|
|
1696
1722
|
const body = await res.json();
|
|
1697
1723
|
const issues = body.issues ?? [];
|
|
1698
1724
|
return issues.map((it) => {
|
|
@@ -1716,7 +1742,7 @@ var JiraClient = class {
|
|
|
1716
1742
|
accept: "application/json"
|
|
1717
1743
|
};
|
|
1718
1744
|
}
|
|
1719
|
-
mapHttpError(status, id) {
|
|
1745
|
+
mapHttpError(status, id, body = "") {
|
|
1720
1746
|
if (status === 401 || status === 403) {
|
|
1721
1747
|
return new TrackerError(
|
|
1722
1748
|
`Jira authentication failed (HTTP ${status}). Check your email and API token in .squad/secrets.yaml.`,
|
|
@@ -1741,16 +1767,37 @@ var JiraClient = class {
|
|
|
1741
1767
|
status
|
|
1742
1768
|
);
|
|
1743
1769
|
}
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
status
|
|
1748
|
-
);
|
|
1770
|
+
const reason = extractJiraErrorMessages(body);
|
|
1771
|
+
const base = id === "search" ? `Jira search failed (HTTP ${status}).` : `Jira request failed (HTTP ${status}).`;
|
|
1772
|
+
return new TrackerError(reason ? `${base} ${reason}` : base, "other", status);
|
|
1749
1773
|
}
|
|
1750
1774
|
};
|
|
1751
1775
|
function stripHtml2(html) {
|
|
1752
1776
|
return html.replace(/<\s*br\s*\/?>/gi, "\n").replace(/<\/(p|div|li|h[1-6])>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/\n{3,}/g, "\n\n").trim();
|
|
1753
1777
|
}
|
|
1778
|
+
function extractJiraErrorMessages(body) {
|
|
1779
|
+
if (!body) return "";
|
|
1780
|
+
let parsed;
|
|
1781
|
+
try {
|
|
1782
|
+
parsed = JSON.parse(body);
|
|
1783
|
+
} catch {
|
|
1784
|
+
return body.slice(0, 200);
|
|
1785
|
+
}
|
|
1786
|
+
const messages = [];
|
|
1787
|
+
if (Array.isArray(parsed.errorMessages)) {
|
|
1788
|
+
for (const m of parsed.errorMessages) {
|
|
1789
|
+
if (typeof m === "string" && m.trim().length > 0) messages.push(m.trim());
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
if (parsed.errors && typeof parsed.errors === "object") {
|
|
1793
|
+
for (const [k, v] of Object.entries(parsed.errors)) {
|
|
1794
|
+
if (typeof v === "string" && v.trim().length > 0) messages.push(`${k}: ${v.trim()}`);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
if (messages.length === 0) return "";
|
|
1798
|
+
const joined = messages.join(" ");
|
|
1799
|
+
return joined.length > 200 ? `${joined.slice(0, 197)}...` : joined;
|
|
1800
|
+
}
|
|
1754
1801
|
|
|
1755
1802
|
// src/tracker/index.ts
|
|
1756
1803
|
function clientFor(config2, secrets) {
|
|
@@ -3108,12 +3155,12 @@ async function tryFetchIssue(config2, id, opts, paths, attachmentsDir) {
|
|
|
3108
3155
|
let issue;
|
|
3109
3156
|
try {
|
|
3110
3157
|
issue = await client.fetchIssue(id);
|
|
3111
|
-
} catch (
|
|
3158
|
+
} catch (err2) {
|
|
3112
3159
|
spin.fail("Tracker fetch failed");
|
|
3113
|
-
if (
|
|
3114
|
-
return { outcome: "failed", reason:
|
|
3160
|
+
if (err2 instanceof TrackerError) {
|
|
3161
|
+
return { outcome: "failed", reason: err2.message, hint: explainTrackerErrorHint(err2) };
|
|
3115
3162
|
}
|
|
3116
|
-
return { outcome: "failed", reason:
|
|
3163
|
+
return { outcome: "failed", reason: err2.message };
|
|
3117
3164
|
}
|
|
3118
3165
|
spin.succeed(`Fetched ${issue.id}: ${truncate(issue.title, 60)}`);
|
|
3119
3166
|
let downloads = [];
|
|
@@ -3135,8 +3182,8 @@ async function tryFetchIssue(config2, id, opts, paths, attachmentsDir) {
|
|
|
3135
3182
|
}
|
|
3136
3183
|
return { outcome: "ok", issue, downloads };
|
|
3137
3184
|
}
|
|
3138
|
-
function explainTrackerErrorHint(
|
|
3139
|
-
switch (
|
|
3185
|
+
function explainTrackerErrorHint(err2) {
|
|
3186
|
+
switch (err2.kind) {
|
|
3140
3187
|
case "auth":
|
|
3141
3188
|
return "Run `squad config set tracker` to re-enter credentials, or `squad doctor` to verify.";
|
|
3142
3189
|
case "not-found":
|
|
@@ -3154,8 +3201,8 @@ function truncate(s, n) {
|
|
|
3154
3201
|
}
|
|
3155
3202
|
|
|
3156
3203
|
// src/commands/new-plan.ts
|
|
3157
|
-
import
|
|
3158
|
-
import
|
|
3204
|
+
import fs31 from "fs";
|
|
3205
|
+
import path23 from "path";
|
|
3159
3206
|
import { confirm as confirm6, select as select6 } from "@inquirer/prompts";
|
|
3160
3207
|
|
|
3161
3208
|
// src/utils/clipboard.ts
|
|
@@ -3183,16 +3230,16 @@ function tryCopy(cmd, text) {
|
|
|
3183
3230
|
let child;
|
|
3184
3231
|
try {
|
|
3185
3232
|
child = spawn(cmd.command, cmd.args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
3186
|
-
} catch (
|
|
3187
|
-
resolve2({ ok: false, reason: `${cmd.command} failed to spawn: ${
|
|
3233
|
+
} catch (err2) {
|
|
3234
|
+
resolve2({ ok: false, reason: `${cmd.command} failed to spawn: ${err2.message}` });
|
|
3188
3235
|
return;
|
|
3189
3236
|
}
|
|
3190
3237
|
let stderr = "";
|
|
3191
3238
|
child.stderr?.on("data", (chunk) => {
|
|
3192
3239
|
stderr += chunk.toString();
|
|
3193
3240
|
});
|
|
3194
|
-
child.on("error", (
|
|
3195
|
-
resolve2({ ok: false, reason: `${cmd.command} spawn error: ${
|
|
3241
|
+
child.on("error", (err2) => {
|
|
3242
|
+
resolve2({ ok: false, reason: `${cmd.command} spawn error: ${err2.message}` });
|
|
3196
3243
|
});
|
|
3197
3244
|
child.on("close", (code) => {
|
|
3198
3245
|
if (code === 0) {
|
|
@@ -3247,63 +3294,189 @@ function noCandidateReason() {
|
|
|
3247
3294
|
}
|
|
3248
3295
|
|
|
3249
3296
|
// src/planner/loop.ts
|
|
3250
|
-
import
|
|
3297
|
+
import path19 from "path";
|
|
3298
|
+
import fs27 from "fs";
|
|
3299
|
+
import { APICallError as APICallError2 } from "ai";
|
|
3251
3300
|
|
|
3252
|
-
// src/planner/tools.ts
|
|
3301
|
+
// src/planner/tools/read-file.ts
|
|
3253
3302
|
import fs21 from "fs";
|
|
3254
3303
|
import path13 from "path";
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
required: ["path"],
|
|
3262
|
-
properties: {
|
|
3263
|
-
path: { type: "string", description: "Repo-relative POSIX path." }
|
|
3264
|
-
}
|
|
3265
|
-
}
|
|
3266
|
-
};
|
|
3267
|
-
var MAX_BYTES_PER_READ = 32e3;
|
|
3304
|
+
import { tool } from "ai";
|
|
3305
|
+
import { z } from "zod";
|
|
3306
|
+
var READ_FILE_TOOL_NAME = "read_file";
|
|
3307
|
+
var MAX_BYTES_PER_FULL_READ = 32e3;
|
|
3308
|
+
var MAX_LINES_PER_RANGED_READ = 400;
|
|
3309
|
+
var MAX_BYTES_FOR_SYNC_LINE_SPLIT = 2e6;
|
|
3268
3310
|
function readFileTool(root, budget, input5) {
|
|
3269
|
-
const raw = input5
|
|
3270
|
-
if (
|
|
3271
|
-
return
|
|
3311
|
+
const raw = typeof input5.path === "string" ? input5.path : "";
|
|
3312
|
+
if (!raw.length) {
|
|
3313
|
+
return err('read_file: missing or invalid "path" argument.');
|
|
3314
|
+
}
|
|
3315
|
+
const offset = typeof input5.offset === "number" && Number.isInteger(input5.offset) ? input5.offset : void 0;
|
|
3316
|
+
const limit = typeof input5.limit === "number" && Number.isInteger(input5.limit) ? input5.limit : void 0;
|
|
3317
|
+
if (offset !== void 0 && offset < 1) {
|
|
3318
|
+
return err("read_file: offset must be a 1-indexed line number \u2265 1.");
|
|
3319
|
+
}
|
|
3320
|
+
if (limit !== void 0 && (limit < 1 || limit > MAX_LINES_PER_RANGED_READ)) {
|
|
3321
|
+
return err(`read_file: limit must be between 1 and ${MAX_LINES_PER_RANGED_READ}.`);
|
|
3322
|
+
}
|
|
3323
|
+
if (offset === void 0 !== (limit === void 0)) {
|
|
3324
|
+
return err("read_file: pass both offset and limit for a ranged read, or neither for a full read.");
|
|
3272
3325
|
}
|
|
3273
3326
|
const resolved = path13.resolve(root, raw);
|
|
3274
3327
|
const relCheck = path13.relative(root, resolved);
|
|
3275
3328
|
if (relCheck.startsWith("..") || path13.isAbsolute(relCheck)) {
|
|
3276
|
-
return
|
|
3329
|
+
return err(`read_file: path "${raw}" escapes the project root and was refused.`);
|
|
3277
3330
|
}
|
|
3278
3331
|
if (!fs21.existsSync(resolved)) {
|
|
3279
|
-
return
|
|
3332
|
+
return err(`read_file: not found "${raw}".`);
|
|
3280
3333
|
}
|
|
3281
3334
|
const stat = fs21.statSync(resolved);
|
|
3282
3335
|
if (stat.isDirectory()) {
|
|
3283
|
-
return
|
|
3336
|
+
return err(`read_file: "${raw}" is a directory. Ask for a specific file path.`);
|
|
3337
|
+
}
|
|
3338
|
+
if (offset !== void 0 && limit !== void 0) {
|
|
3339
|
+
return readFileRanged(resolved, raw, budget, stat.size, offset, limit);
|
|
3284
3340
|
}
|
|
3285
|
-
if (stat.size >
|
|
3286
|
-
return
|
|
3287
|
-
`read_file: "${raw}" is ${stat.size} bytes (> ${
|
|
3341
|
+
if (stat.size > MAX_BYTES_PER_FULL_READ) {
|
|
3342
|
+
return err(
|
|
3343
|
+
`read_file: "${raw}" is ${stat.size} bytes (> ${MAX_BYTES_PER_FULL_READ}). Use offset/limit for a ranged read or ask for a smaller region.`
|
|
3288
3344
|
);
|
|
3289
3345
|
}
|
|
3290
3346
|
const capacity = budget.canRead(stat.size);
|
|
3291
3347
|
if (!capacity.ok) {
|
|
3292
|
-
return
|
|
3348
|
+
return err(`read_file: ${capacity.reason}. Finalise the plan with what you already have.`);
|
|
3293
3349
|
}
|
|
3294
3350
|
let buf;
|
|
3295
3351
|
try {
|
|
3296
3352
|
buf = fs21.readFileSync(resolved);
|
|
3297
3353
|
} catch (e) {
|
|
3298
|
-
return
|
|
3354
|
+
return err(`read_file: failed to read "${raw}" (${e.message}).`);
|
|
3299
3355
|
}
|
|
3300
3356
|
if (looksBinary(buf)) {
|
|
3301
|
-
return
|
|
3357
|
+
return err(`read_file: "${raw}" looks binary and was refused.`);
|
|
3302
3358
|
}
|
|
3303
3359
|
budget.recordRead(stat.size);
|
|
3304
3360
|
return { content: buf.toString("utf8"), isError: false };
|
|
3305
3361
|
}
|
|
3306
|
-
function
|
|
3362
|
+
function readFileRanged(resolved, rawRel, budget, fileSize, offset, limit) {
|
|
3363
|
+
const est = Math.min(fileSize, limit * 200);
|
|
3364
|
+
const cap = budget.canRead(est);
|
|
3365
|
+
if (!cap.ok) {
|
|
3366
|
+
return err(`read_file: ${cap.reason}. Finalise the plan with what you already have.`);
|
|
3367
|
+
}
|
|
3368
|
+
if (fileSize > MAX_BYTES_FOR_SYNC_LINE_SPLIT) {
|
|
3369
|
+
return err(
|
|
3370
|
+
`read_file: "${rawRel}" is too large for a ranged read in-process (${fileSize} bytes). Grep for a symbol or narrow the path.`
|
|
3371
|
+
);
|
|
3372
|
+
}
|
|
3373
|
+
let data;
|
|
3374
|
+
try {
|
|
3375
|
+
data = fs21.readFileSync(resolved);
|
|
3376
|
+
} catch (e) {
|
|
3377
|
+
return err(`read_file: failed to read "${rawRel}" (${e.message}).`);
|
|
3378
|
+
}
|
|
3379
|
+
if (looksBinary(data)) {
|
|
3380
|
+
return err(`read_file: "${rawRel}" looks binary and was refused.`);
|
|
3381
|
+
}
|
|
3382
|
+
const text = data.toString("utf8");
|
|
3383
|
+
const lines = text.split(/\r?\n/);
|
|
3384
|
+
if (offset > lines.length) {
|
|
3385
|
+
return err(
|
|
3386
|
+
`read_file: offset ${offset} is past the end of the file (only ${lines.length} lines).`
|
|
3387
|
+
);
|
|
3388
|
+
}
|
|
3389
|
+
const slice = lines.slice(offset - 1, offset - 1 + limit);
|
|
3390
|
+
const out = slice.map((line2, i) => `${String(offset + i).padStart(6, " ")}\u2502 ${line2}`).join("\n");
|
|
3391
|
+
const bytes = Buffer.byteLength(out, "utf8");
|
|
3392
|
+
budget.recordRead(bytes);
|
|
3393
|
+
return { content: out, isError: false };
|
|
3394
|
+
}
|
|
3395
|
+
function readFileToolFactory(root, budget, hooks, opts) {
|
|
3396
|
+
const ranged = opts?.ranged ?? true;
|
|
3397
|
+
const params = ranged ? z.object({
|
|
3398
|
+
path: z.string().min(1).describe("Repo-relative POSIX path."),
|
|
3399
|
+
offset: z.number().int().min(1).optional().describe("1-indexed line number to start reading at (use with limit)."),
|
|
3400
|
+
limit: z.number().int().min(1).max(MAX_LINES_PER_RANGED_READ).optional().describe(`Line count from offset (max ${MAX_LINES_PER_RANGED_READ}).`)
|
|
3401
|
+
}).refine(
|
|
3402
|
+
(o) => o.offset === void 0 && o.limit === void 0 || o.offset !== void 0 && o.limit !== void 0,
|
|
3403
|
+
{ message: "Provide both offset and limit for a ranged read, or neither for a full read." }
|
|
3404
|
+
) : z.object({
|
|
3405
|
+
path: z.string().min(1).describe("Repo-relative POSIX path.")
|
|
3406
|
+
});
|
|
3407
|
+
return tool({
|
|
3408
|
+
description: "Read a UTF-8 text file from the project, returning its contents so you can plan against real code. " + (ranged ? "Pass `offset` and `limit` together to read a line range (preferred when you know the region). Without them the whole file is read, capped at 32 KB. " : "Whole-file reads only; capped at 32 KB. ") + "Paths must be relative to the project root. Binary files are refused. You have a bounded context budget; prefer small, targeted reads.",
|
|
3409
|
+
parameters: params,
|
|
3410
|
+
execute: async (args, { toolCallId }) => {
|
|
3411
|
+
const tc = {
|
|
3412
|
+
id: toolCallId,
|
|
3413
|
+
name: READ_FILE_TOOL_NAME,
|
|
3414
|
+
input: { path: args.path, offset: args.offset, limit: args.limit }
|
|
3415
|
+
};
|
|
3416
|
+
const before = budget.snapshot().bytes;
|
|
3417
|
+
let result = readFileTool(root, budget, args);
|
|
3418
|
+
let after = budget.snapshot().bytes;
|
|
3419
|
+
let bytesLoaded = after - before;
|
|
3420
|
+
hooks.onToolCall?.(tc, bytesLoaded, after);
|
|
3421
|
+
hooks.bus.emit({
|
|
3422
|
+
kind: "tool_call",
|
|
3423
|
+
runId: hooks.runId,
|
|
3424
|
+
turn: hooks.turn,
|
|
3425
|
+
toolCall: tc,
|
|
3426
|
+
bytesLoaded,
|
|
3427
|
+
totalBytes: after
|
|
3428
|
+
});
|
|
3429
|
+
if (readBudgetishError(result)) {
|
|
3430
|
+
const decide = hooks.decideOnLimit;
|
|
3431
|
+
if (decide) {
|
|
3432
|
+
const d = await decide(hooks.getLimitCtx());
|
|
3433
|
+
if (d === "cancel") {
|
|
3434
|
+
throw new PlannerUserCancelledError(
|
|
3435
|
+
hooks.getAccumulatedText(),
|
|
3436
|
+
hooks.runId,
|
|
3437
|
+
hooks.turn,
|
|
3438
|
+
"budget_reads",
|
|
3439
|
+
tc,
|
|
3440
|
+
result
|
|
3441
|
+
);
|
|
3442
|
+
}
|
|
3443
|
+
hooks.extendSessionLimits();
|
|
3444
|
+
result = readFileTool(root, budget, args);
|
|
3445
|
+
after = budget.snapshot().bytes;
|
|
3446
|
+
bytesLoaded = after - before;
|
|
3447
|
+
} else {
|
|
3448
|
+
hooks.setBudgetExhausted(true);
|
|
3449
|
+
}
|
|
3450
|
+
}
|
|
3451
|
+
return { content: result.content, isError: result.isError };
|
|
3452
|
+
}
|
|
3453
|
+
});
|
|
3454
|
+
}
|
|
3455
|
+
var PlannerUserCancelledError = class _PlannerUserCancelledError extends Error {
|
|
3456
|
+
constructor(planText, runId, turn, incompleteKind, toolCall, pendingResult) {
|
|
3457
|
+
super("PlannerUserCancelledError");
|
|
3458
|
+
this.planText = planText;
|
|
3459
|
+
this.runId = runId;
|
|
3460
|
+
this.turn = turn;
|
|
3461
|
+
this.incompleteKind = incompleteKind;
|
|
3462
|
+
this.toolCall = toolCall;
|
|
3463
|
+
this.pendingResult = pendingResult;
|
|
3464
|
+
this.name = "PlannerUserCancelledError";
|
|
3465
|
+
}
|
|
3466
|
+
planText;
|
|
3467
|
+
runId;
|
|
3468
|
+
turn;
|
|
3469
|
+
incompleteKind;
|
|
3470
|
+
toolCall;
|
|
3471
|
+
pendingResult;
|
|
3472
|
+
static is(e) {
|
|
3473
|
+
return e instanceof _PlannerUserCancelledError;
|
|
3474
|
+
}
|
|
3475
|
+
};
|
|
3476
|
+
function readBudgetishError(result) {
|
|
3477
|
+
return result.isError && (/budget/i.test(result.content) || /max file reads/i.test(result.content));
|
|
3478
|
+
}
|
|
3479
|
+
function err(msg) {
|
|
3307
3480
|
return { content: msg, isError: true };
|
|
3308
3481
|
}
|
|
3309
3482
|
function looksBinary(buf) {
|
|
@@ -3314,6 +3487,436 @@ function looksBinary(buf) {
|
|
|
3314
3487
|
return false;
|
|
3315
3488
|
}
|
|
3316
3489
|
|
|
3490
|
+
// src/planner/tools/grep.ts
|
|
3491
|
+
import fs23 from "fs";
|
|
3492
|
+
import path15 from "path";
|
|
3493
|
+
import { tool as tool2 } from "ai";
|
|
3494
|
+
import { z as z2 } from "zod";
|
|
3495
|
+
|
|
3496
|
+
// src/core/repo-map.ts
|
|
3497
|
+
import fs22 from "fs";
|
|
3498
|
+
import path14 from "path";
|
|
3499
|
+
import ignore from "ignore";
|
|
3500
|
+
var DEFAULT_IGNORES = [
|
|
3501
|
+
"node_modules",
|
|
3502
|
+
".git",
|
|
3503
|
+
"dist",
|
|
3504
|
+
"build",
|
|
3505
|
+
"coverage",
|
|
3506
|
+
".next",
|
|
3507
|
+
".turbo",
|
|
3508
|
+
".cache",
|
|
3509
|
+
".squad/plans"
|
|
3510
|
+
// don't include existing plans as context; planner will see them separately
|
|
3511
|
+
];
|
|
3512
|
+
function buildRepoIgnore(root) {
|
|
3513
|
+
const ig = ignore().add(DEFAULT_IGNORES);
|
|
3514
|
+
const gitignore = path14.join(root, ".gitignore");
|
|
3515
|
+
if (fs22.existsSync(gitignore)) {
|
|
3516
|
+
try {
|
|
3517
|
+
ig.add(fs22.readFileSync(gitignore, "utf8"));
|
|
3518
|
+
} catch {
|
|
3519
|
+
}
|
|
3520
|
+
}
|
|
3521
|
+
return ig;
|
|
3522
|
+
}
|
|
3523
|
+
function buildRepoMap(root, opts = {}) {
|
|
3524
|
+
const maxEntries = opts.maxEntries ?? 5e3;
|
|
3525
|
+
const format = opts.format ?? "flat";
|
|
3526
|
+
const ig = buildRepoIgnore(root);
|
|
3527
|
+
if (format === "tree") {
|
|
3528
|
+
const entries2 = [];
|
|
3529
|
+
collectFileEntries(root, root, ig, entries2, maxEntries, !!opts.includeHidden);
|
|
3530
|
+
entries2.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
3531
|
+
return renderRepoTree(entries2) + "\n";
|
|
3532
|
+
}
|
|
3533
|
+
const entries = [];
|
|
3534
|
+
walkFlat(root, root, ig, entries, maxEntries, !!opts.includeHidden);
|
|
3535
|
+
entries.sort();
|
|
3536
|
+
return entries.join("\n") + "\n";
|
|
3537
|
+
}
|
|
3538
|
+
function formatSizeKb(n) {
|
|
3539
|
+
if (n < 1024) return `${n} B`;
|
|
3540
|
+
return `${(n / 1024).toFixed(1)} KB`;
|
|
3541
|
+
}
|
|
3542
|
+
function insertTree(tree, relParts, size) {
|
|
3543
|
+
if (relParts.length === 1) {
|
|
3544
|
+
tree.files.push({ name: relParts[0], size });
|
|
3545
|
+
return;
|
|
3546
|
+
}
|
|
3547
|
+
const [head, ...rest] = relParts;
|
|
3548
|
+
if (!tree.dirs.has(head)) tree.dirs.set(head, { files: [], dirs: /* @__PURE__ */ new Map() });
|
|
3549
|
+
insertTree(tree.dirs.get(head), rest, size);
|
|
3550
|
+
}
|
|
3551
|
+
function renderDirTree(name, tree, depth) {
|
|
3552
|
+
const lines = [];
|
|
3553
|
+
const indent = " ".repeat(depth);
|
|
3554
|
+
if (name) {
|
|
3555
|
+
lines.push(`${indent}${name}/`);
|
|
3556
|
+
}
|
|
3557
|
+
const innerIndent = " ".repeat(name ? depth + 1 : depth);
|
|
3558
|
+
const dirNames = [...tree.dirs.keys()].sort((a, b) => a.localeCompare(b));
|
|
3559
|
+
for (const d of dirNames) {
|
|
3560
|
+
lines.push(...renderDirTree(d, tree.dirs.get(d), name ? depth + 1 : depth));
|
|
3561
|
+
}
|
|
3562
|
+
tree.files.sort((a, b) => a.name.localeCompare(b.name));
|
|
3563
|
+
for (const f of tree.files) {
|
|
3564
|
+
const left = `${innerIndent}${f.name}`;
|
|
3565
|
+
const pad2 = Math.max(1, 48 - left.length);
|
|
3566
|
+
lines.push(`${left}${" ".repeat(pad2)}${formatSizeKb(f.size)}`);
|
|
3567
|
+
}
|
|
3568
|
+
return lines;
|
|
3569
|
+
}
|
|
3570
|
+
function renderRepoTree(entries) {
|
|
3571
|
+
const root = { files: [], dirs: /* @__PURE__ */ new Map() };
|
|
3572
|
+
for (const e of entries) {
|
|
3573
|
+
const parts = e.rel.split(path14.sep).filter(Boolean);
|
|
3574
|
+
if (parts.length === 0) continue;
|
|
3575
|
+
insertTree(root, parts, e.size);
|
|
3576
|
+
}
|
|
3577
|
+
return renderDirTree("", root, 0).join("\n");
|
|
3578
|
+
}
|
|
3579
|
+
function collectFileEntries(root, dir, ig, out, max, includeHidden) {
|
|
3580
|
+
if (out.length >= max) return out;
|
|
3581
|
+
let dirents;
|
|
3582
|
+
try {
|
|
3583
|
+
dirents = fs22.readdirSync(dir, { withFileTypes: true });
|
|
3584
|
+
} catch {
|
|
3585
|
+
return out;
|
|
3586
|
+
}
|
|
3587
|
+
for (const entry of dirents) {
|
|
3588
|
+
if (out.length >= max) break;
|
|
3589
|
+
if (!includeHidden && entry.name.startsWith(".") && entry.name !== ".squad") continue;
|
|
3590
|
+
const abs = path14.join(dir, entry.name);
|
|
3591
|
+
const rel = path14.relative(root, abs);
|
|
3592
|
+
if (!rel || ig.ignores(rel) || entry.isDirectory() && ig.ignores(rel + "/")) continue;
|
|
3593
|
+
if (entry.isDirectory()) {
|
|
3594
|
+
collectFileEntries(root, abs, ig, out, max, includeHidden);
|
|
3595
|
+
} else if (entry.isFile()) {
|
|
3596
|
+
let size = 0;
|
|
3597
|
+
try {
|
|
3598
|
+
size = fs22.statSync(abs).size;
|
|
3599
|
+
} catch {
|
|
3600
|
+
size = 0;
|
|
3601
|
+
}
|
|
3602
|
+
out.push({ rel: rel.split(path14.sep).join("/"), size });
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
return out;
|
|
3606
|
+
}
|
|
3607
|
+
function walkFlat(root, dir, ig, out, max, includeHidden) {
|
|
3608
|
+
if (out.length >= max) return;
|
|
3609
|
+
let entries;
|
|
3610
|
+
try {
|
|
3611
|
+
entries = fs22.readdirSync(dir, { withFileTypes: true });
|
|
3612
|
+
} catch {
|
|
3613
|
+
return;
|
|
3614
|
+
}
|
|
3615
|
+
for (const entry of entries) {
|
|
3616
|
+
if (out.length >= max) return;
|
|
3617
|
+
if (!includeHidden && entry.name.startsWith(".") && entry.name !== ".squad") continue;
|
|
3618
|
+
const abs = path14.join(dir, entry.name);
|
|
3619
|
+
const rel = path14.relative(root, abs);
|
|
3620
|
+
if (!rel || ig.ignores(rel) || entry.isDirectory() && ig.ignores(rel + "/")) continue;
|
|
3621
|
+
if (entry.isDirectory()) {
|
|
3622
|
+
walkFlat(root, abs, ig, out, max, includeHidden);
|
|
3623
|
+
} else if (entry.isFile()) {
|
|
3624
|
+
out.push(rel.split(path14.sep).join("/"));
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3629
|
+
// src/planner/tools/grep.ts
|
|
3630
|
+
var MAX_GREP_MATCHES = 200;
|
|
3631
|
+
var MAX_GREP_FILE_SIZE_BYTES = 1e6;
|
|
3632
|
+
var GREP_TOOL_NAME = "grep";
|
|
3633
|
+
function grepToolFactory(root, budget, hooks) {
|
|
3634
|
+
return tool2({
|
|
3635
|
+
description: "Search the project for a literal string or regex pattern. Returns up to 200 matches across files, each shown as `path:line:match`. Use this BEFORE `read_file` to locate symbols or call sites; it is much cheaper than reading whole files.",
|
|
3636
|
+
parameters: z2.object({
|
|
3637
|
+
pattern: z2.string().min(1).describe("Literal string or regex (default literal)."),
|
|
3638
|
+
regex: z2.boolean().optional().default(false),
|
|
3639
|
+
path: z2.string().optional().describe("Optional sub-path to scope the search to (relative)."),
|
|
3640
|
+
caseInsensitive: z2.boolean().optional().default(false)
|
|
3641
|
+
}),
|
|
3642
|
+
execute: async (args, { toolCallId }) => {
|
|
3643
|
+
const tc = {
|
|
3644
|
+
id: toolCallId,
|
|
3645
|
+
name: GREP_TOOL_NAME,
|
|
3646
|
+
input: { ...args }
|
|
3647
|
+
};
|
|
3648
|
+
const before = budget.snapshot().bytes;
|
|
3649
|
+
const out = runGrep(root, budget, args);
|
|
3650
|
+
const after = budget.snapshot().bytes;
|
|
3651
|
+
const bytesLoaded = after - before;
|
|
3652
|
+
hooks.onToolCall?.(tc, bytesLoaded, after);
|
|
3653
|
+
hooks.bus.emit({
|
|
3654
|
+
kind: "tool_call",
|
|
3655
|
+
runId: hooks.runId,
|
|
3656
|
+
turn: hooks.turn,
|
|
3657
|
+
toolCall: tc,
|
|
3658
|
+
bytesLoaded,
|
|
3659
|
+
totalBytes: after
|
|
3660
|
+
});
|
|
3661
|
+
return out;
|
|
3662
|
+
}
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
function runGrep(root, budget, args) {
|
|
3666
|
+
const exec = budget.canExecuteTool();
|
|
3667
|
+
if (!exec.ok) {
|
|
3668
|
+
return {
|
|
3669
|
+
content: `grep: ${exec.reason}. Finalise the plan with what you already have.`,
|
|
3670
|
+
isError: true
|
|
3671
|
+
};
|
|
3672
|
+
}
|
|
3673
|
+
let re;
|
|
3674
|
+
try {
|
|
3675
|
+
if (args.regex) {
|
|
3676
|
+
re = new RegExp(args.pattern, args.caseInsensitive ? "gi" : "g");
|
|
3677
|
+
} else {
|
|
3678
|
+
const esc = args.pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3679
|
+
re = new RegExp(esc, args.caseInsensitive ? "gi" : "g");
|
|
3680
|
+
}
|
|
3681
|
+
} catch (e) {
|
|
3682
|
+
return { content: `grep: invalid pattern (${e.message}).`, isError: true };
|
|
3683
|
+
}
|
|
3684
|
+
const ig = buildRepoIgnore(root);
|
|
3685
|
+
const scopeRoot = args.path?.trim() ? path15.resolve(root, args.path) : root;
|
|
3686
|
+
const relScope = path15.relative(root, scopeRoot);
|
|
3687
|
+
if (relScope.startsWith("..") || path15.isAbsolute(relScope)) {
|
|
3688
|
+
return { content: `grep: scope path escapes the project root and was refused.`, isError: true };
|
|
3689
|
+
}
|
|
3690
|
+
if (!fs23.existsSync(scopeRoot)) {
|
|
3691
|
+
return { content: `grep: scope path not found "${args.path}".`, isError: true };
|
|
3692
|
+
}
|
|
3693
|
+
const matches = [];
|
|
3694
|
+
const walk2 = (dir) => {
|
|
3695
|
+
if (matches.length >= MAX_GREP_MATCHES) return;
|
|
3696
|
+
let entries;
|
|
3697
|
+
try {
|
|
3698
|
+
entries = fs23.readdirSync(dir, { withFileTypes: true });
|
|
3699
|
+
} catch {
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3702
|
+
for (const entry of entries) {
|
|
3703
|
+
if (matches.length >= MAX_GREP_MATCHES) return;
|
|
3704
|
+
const abs = path15.join(dir, entry.name);
|
|
3705
|
+
const rel = path15.relative(root, abs).split(path15.sep).join("/");
|
|
3706
|
+
if (!rel || ig.ignores(rel) || entry.isDirectory() && ig.ignores(rel + "/")) continue;
|
|
3707
|
+
if (entry.isDirectory()) {
|
|
3708
|
+
walk2(abs);
|
|
3709
|
+
} else if (entry.isFile()) {
|
|
3710
|
+
let st;
|
|
3711
|
+
try {
|
|
3712
|
+
st = fs23.statSync(abs);
|
|
3713
|
+
} catch {
|
|
3714
|
+
continue;
|
|
3715
|
+
}
|
|
3716
|
+
if (st.size > MAX_GREP_FILE_SIZE_BYTES) continue;
|
|
3717
|
+
let buf;
|
|
3718
|
+
try {
|
|
3719
|
+
buf = fs23.readFileSync(abs);
|
|
3720
|
+
} catch {
|
|
3721
|
+
continue;
|
|
3722
|
+
}
|
|
3723
|
+
if (looksBinary(buf)) continue;
|
|
3724
|
+
const text = buf.toString("utf8");
|
|
3725
|
+
const lines = text.split(/\r?\n/);
|
|
3726
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3727
|
+
if (matches.length >= MAX_GREP_MATCHES) return;
|
|
3728
|
+
const line2 = lines[i] ?? "";
|
|
3729
|
+
re.lastIndex = 0;
|
|
3730
|
+
if (re.test(line2)) {
|
|
3731
|
+
const trimmed = line2.trim().slice(0, 200);
|
|
3732
|
+
matches.push(`${rel}:${i + 1}:${trimmed}`);
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
};
|
|
3738
|
+
if (fs23.statSync(scopeRoot).isFile()) {
|
|
3739
|
+
const rel = path15.relative(root, scopeRoot).split(path15.sep).join("/");
|
|
3740
|
+
if (ig.ignores(rel)) {
|
|
3741
|
+
return { content: `grep: file is ignored by repository filters.`, isError: true };
|
|
3742
|
+
}
|
|
3743
|
+
let st;
|
|
3744
|
+
try {
|
|
3745
|
+
st = fs23.statSync(scopeRoot);
|
|
3746
|
+
} catch {
|
|
3747
|
+
return { content: `grep: not found.`, isError: true };
|
|
3748
|
+
}
|
|
3749
|
+
if (st.size > MAX_GREP_FILE_SIZE_BYTES) {
|
|
3750
|
+
return { content: `grep: file too large to search.`, isError: true };
|
|
3751
|
+
}
|
|
3752
|
+
let buf;
|
|
3753
|
+
try {
|
|
3754
|
+
buf = fs23.readFileSync(scopeRoot);
|
|
3755
|
+
} catch (e) {
|
|
3756
|
+
return { content: `grep: failed to read (${e.message}).`, isError: true };
|
|
3757
|
+
}
|
|
3758
|
+
if (looksBinary(buf)) return { content: `grep: file looks binary and was skipped.`, isError: true };
|
|
3759
|
+
const text = buf.toString("utf8");
|
|
3760
|
+
const lines = text.split(/\r?\n/);
|
|
3761
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3762
|
+
if (matches.length >= MAX_GREP_MATCHES) break;
|
|
3763
|
+
const line2 = lines[i] ?? "";
|
|
3764
|
+
re.lastIndex = 0;
|
|
3765
|
+
if (re.test(line2)) {
|
|
3766
|
+
matches.push(`${rel}:${i + 1}:${line2.trim().slice(0, 200)}`);
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
} else {
|
|
3770
|
+
walk2(scopeRoot);
|
|
3771
|
+
}
|
|
3772
|
+
const header = matches.length >= MAX_GREP_MATCHES ? `Found ${matches.length} matches (capped at ${MAX_GREP_MATCHES}):` : `Found ${matches.length} match${matches.length === 1 ? "" : "es"}:`;
|
|
3773
|
+
const body = matches.length ? `${header}
|
|
3774
|
+
${matches.join("\n")}` : `No matches found for "${args.pattern}".`;
|
|
3775
|
+
const byteSize = Buffer.byteLength(body, "utf8");
|
|
3776
|
+
const cap = budget.canRead(byteSize);
|
|
3777
|
+
if (!cap.ok) {
|
|
3778
|
+
return {
|
|
3779
|
+
content: `grep: ${cap.reason}. Finalise the plan with what you already have.`,
|
|
3780
|
+
isError: true
|
|
3781
|
+
};
|
|
3782
|
+
}
|
|
3783
|
+
budget.recordRead(byteSize);
|
|
3784
|
+
return { content: body, isError: false };
|
|
3785
|
+
}
|
|
3786
|
+
|
|
3787
|
+
// src/planner/tools/list-dir.ts
|
|
3788
|
+
import fs24 from "fs";
|
|
3789
|
+
import path16 from "path";
|
|
3790
|
+
import { tool as tool3 } from "ai";
|
|
3791
|
+
import { z as z3 } from "zod";
|
|
3792
|
+
var MAX_ENTRIES_PER_LIST = 200;
|
|
3793
|
+
var LIST_DIR_TOOL_NAME = "list_dir";
|
|
3794
|
+
function listDirToolFactory(root, budget, hooks) {
|
|
3795
|
+
return tool3({
|
|
3796
|
+
description: "List the contents of a directory in the project (one level deep). Returns up to 200 entries with type (file/dir) and size. Cheap; use this to discover sibling files before reading them.",
|
|
3797
|
+
parameters: z3.object({
|
|
3798
|
+
path: z3.string().describe('Repo-relative directory path. Use "." for the project root.')
|
|
3799
|
+
}),
|
|
3800
|
+
execute: async (args, { toolCallId }) => {
|
|
3801
|
+
const tc = {
|
|
3802
|
+
id: toolCallId,
|
|
3803
|
+
name: LIST_DIR_TOOL_NAME,
|
|
3804
|
+
input: { path: args.path }
|
|
3805
|
+
};
|
|
3806
|
+
const before = budget.snapshot().bytes;
|
|
3807
|
+
const out = runListDir(root, budget, args.path);
|
|
3808
|
+
const after = budget.snapshot().bytes;
|
|
3809
|
+
hooks.onToolCall?.(tc, after - before, after);
|
|
3810
|
+
hooks.bus.emit({
|
|
3811
|
+
kind: "tool_call",
|
|
3812
|
+
runId: hooks.runId,
|
|
3813
|
+
turn: hooks.turn,
|
|
3814
|
+
toolCall: tc,
|
|
3815
|
+
bytesLoaded: after - before,
|
|
3816
|
+
totalBytes: after
|
|
3817
|
+
});
|
|
3818
|
+
return out;
|
|
3819
|
+
}
|
|
3820
|
+
});
|
|
3821
|
+
}
|
|
3822
|
+
function runListDir(root, budget, rawPath) {
|
|
3823
|
+
const exec = budget.canExecuteTool();
|
|
3824
|
+
if (!exec.ok) {
|
|
3825
|
+
return {
|
|
3826
|
+
content: `list_dir: ${exec.reason}. Finalise the plan with what you already have.`,
|
|
3827
|
+
isError: true
|
|
3828
|
+
};
|
|
3829
|
+
}
|
|
3830
|
+
const normalized = rawPath.trim() === "" ? "." : rawPath.trim();
|
|
3831
|
+
const resolved = path16.resolve(root, normalized);
|
|
3832
|
+
const relCheck = path16.relative(root, resolved);
|
|
3833
|
+
if (relCheck.startsWith("..") || path16.isAbsolute(relCheck)) {
|
|
3834
|
+
return {
|
|
3835
|
+
content: `list_dir: path "${rawPath}" escapes the project root and was refused.`,
|
|
3836
|
+
isError: true
|
|
3837
|
+
};
|
|
3838
|
+
}
|
|
3839
|
+
const relNorm = relCheck.split(path16.sep).join("/");
|
|
3840
|
+
const ig = buildRepoIgnore(root);
|
|
3841
|
+
if (relNorm && (ig.ignores(relNorm) || ig.ignores(`${relNorm}/`))) {
|
|
3842
|
+
return { content: "list_dir: (directory ignored by repo-map filter)", isError: true };
|
|
3843
|
+
}
|
|
3844
|
+
if (!fs24.existsSync(resolved)) {
|
|
3845
|
+
return { content: `list_dir: not found "${rawPath}".`, isError: true };
|
|
3846
|
+
}
|
|
3847
|
+
const st = fs24.statSync(resolved);
|
|
3848
|
+
if (!st.isDirectory()) {
|
|
3849
|
+
return { content: `list_dir: "${rawPath}" is not a directory.`, isError: true };
|
|
3850
|
+
}
|
|
3851
|
+
let dirents;
|
|
3852
|
+
try {
|
|
3853
|
+
dirents = fs24.readdirSync(resolved, { withFileTypes: true });
|
|
3854
|
+
} catch (e) {
|
|
3855
|
+
return { content: `list_dir: failed to read (${e.message}).`, isError: true };
|
|
3856
|
+
}
|
|
3857
|
+
const rows = [];
|
|
3858
|
+
for (const d of dirents) {
|
|
3859
|
+
if (rows.length >= MAX_ENTRIES_PER_LIST) break;
|
|
3860
|
+
const childAbs = path16.join(resolved, d.name);
|
|
3861
|
+
const childRel = path16.relative(root, childAbs).split(path16.sep).join("/");
|
|
3862
|
+
if (childRel && (ig.ignores(childRel) || d.isDirectory() && ig.ignores(`${childRel}/`))) continue;
|
|
3863
|
+
if (d.isDirectory()) {
|
|
3864
|
+
rows.push({ type: "d", name: d.name + "/", size: null });
|
|
3865
|
+
} else if (d.isFile()) {
|
|
3866
|
+
let size = 0;
|
|
3867
|
+
try {
|
|
3868
|
+
size = fs24.statSync(childAbs).size;
|
|
3869
|
+
} catch {
|
|
3870
|
+
size = 0;
|
|
3871
|
+
}
|
|
3872
|
+
rows.push({ type: "f", name: d.name, size });
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
rows.sort((a, b) => {
|
|
3876
|
+
if (a.type !== b.type) return a.type === "d" ? -1 : 1;
|
|
3877
|
+
return a.name.localeCompare(b.name);
|
|
3878
|
+
});
|
|
3879
|
+
const lines = rows.map((r) => {
|
|
3880
|
+
const sz = r.type === "d" ? "\u2014" : String(r.size ?? 0);
|
|
3881
|
+
return `${r.type} ${sz.padStart(8, " ")} ${r.name}`;
|
|
3882
|
+
});
|
|
3883
|
+
const body = lines.length > 0 ? `Entries (max ${MAX_ENTRIES_PER_LIST}):
|
|
3884
|
+
${lines.join("\n")}` : "(empty directory)";
|
|
3885
|
+
const byteSize = Buffer.byteLength(body, "utf8");
|
|
3886
|
+
const cap = budget.canRead(byteSize);
|
|
3887
|
+
if (!cap.ok) {
|
|
3888
|
+
return {
|
|
3889
|
+
content: `list_dir: ${cap.reason}. Finalise the plan with what you already have.`,
|
|
3890
|
+
isError: true
|
|
3891
|
+
};
|
|
3892
|
+
}
|
|
3893
|
+
budget.recordRead(byteSize);
|
|
3894
|
+
return { content: body, isError: false };
|
|
3895
|
+
}
|
|
3896
|
+
|
|
3897
|
+
// src/planner/tools/index.ts
|
|
3898
|
+
function buildPlannerTools(opts) {
|
|
3899
|
+
const enabled = opts.enabled ?? { rangedRead: true, grep: true, listDir: true };
|
|
3900
|
+
const gh = opts.grepHooks ?? {
|
|
3901
|
+
runId: opts.readHooks.runId,
|
|
3902
|
+
turn: opts.readHooks.turn,
|
|
3903
|
+
bus: opts.readHooks.bus,
|
|
3904
|
+
onToolCall: opts.readHooks.onToolCall
|
|
3905
|
+
};
|
|
3906
|
+
const lh = opts.listDirHooks ?? {
|
|
3907
|
+
runId: opts.readHooks.runId,
|
|
3908
|
+
turn: opts.readHooks.turn,
|
|
3909
|
+
bus: opts.readHooks.bus,
|
|
3910
|
+
onToolCall: opts.readHooks.onToolCall
|
|
3911
|
+
};
|
|
3912
|
+
const readOpts = { ranged: enabled.rangedRead ?? true };
|
|
3913
|
+
return {
|
|
3914
|
+
read_file: readFileToolFactory(opts.root, opts.budget, opts.readHooks, readOpts),
|
|
3915
|
+
...enabled.grep ? { grep: grepToolFactory(opts.root, opts.budget, gh) } : {},
|
|
3916
|
+
...enabled.listDir ? { list_dir: listDirToolFactory(opts.root, opts.budget, lh) } : {}
|
|
3917
|
+
};
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3317
3920
|
// src/planner/provider-errors.ts
|
|
3318
3921
|
function detectModelNotFound(provider, model, status, rawBody) {
|
|
3319
3922
|
if (status !== 404) return void 0;
|
|
@@ -3322,16 +3925,16 @@ function detectModelNotFound(provider, model, status, rawBody) {
|
|
|
3322
3925
|
if (!hints.some((h) => body.includes(h))) return void 0;
|
|
3323
3926
|
return { provider, model, status, rawBody };
|
|
3324
3927
|
}
|
|
3325
|
-
function modelNotFoundMessage(
|
|
3928
|
+
function modelNotFoundMessage(err2) {
|
|
3326
3929
|
return [
|
|
3327
|
-
`The ${
|
|
3930
|
+
`The ${err2.provider} planner model "${err2.model}" is no longer available.`,
|
|
3328
3931
|
"",
|
|
3329
3932
|
"Recovery options:",
|
|
3330
3933
|
` 1. Run \`squad upgrade\` to install a newer squad-kit (or install manually from npm).`,
|
|
3331
3934
|
` 2. Run \`squad config set planner --provider openai\` (or another \`--provider\`) to switch models via a different provider, then save a key when prompted.`,
|
|
3332
|
-
` 3. Run \`squad config set planner\` to set \`planner.modelOverride.${
|
|
3935
|
+
` 3. Run \`squad config set planner\` to set \`planner.modelOverride.${err2.provider}\` to a still-valid id (no raw YAML), then \`squad doctor\` to confirm.`,
|
|
3333
3936
|
"",
|
|
3334
|
-
`Raw provider response: ${
|
|
3937
|
+
`Raw provider response: ${err2.rawBody.slice(0, 200)}`
|
|
3335
3938
|
].join("\n");
|
|
3336
3939
|
}
|
|
3337
3940
|
function detectRateLimit(provider, status, headers, rawBody) {
|
|
@@ -3343,16 +3946,20 @@ function detectRateLimit(provider, status, headers, rawBody) {
|
|
|
3343
3946
|
rawBody
|
|
3344
3947
|
};
|
|
3345
3948
|
}
|
|
3346
|
-
function
|
|
3949
|
+
function parseRetryAfterSec(headers) {
|
|
3347
3950
|
const raw = headerGet(headers, "retry-after");
|
|
3348
|
-
if (raw)
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
}
|
|
3951
|
+
if (!raw) return void 0;
|
|
3952
|
+
const sec = Number(raw);
|
|
3953
|
+
if (Number.isFinite(sec) && sec >= 0) return Math.ceil(sec);
|
|
3954
|
+
const asDate = Date.parse(raw);
|
|
3955
|
+
if (Number.isFinite(asDate)) {
|
|
3956
|
+
return Math.max(0, Math.ceil((asDate - Date.now()) / 1e3));
|
|
3355
3957
|
}
|
|
3958
|
+
return void 0;
|
|
3959
|
+
}
|
|
3960
|
+
function parseRetryAfter(headers, body) {
|
|
3961
|
+
const fromHeader = parseRetryAfterSec(headers);
|
|
3962
|
+
if (fromHeader !== void 0) return fromHeader;
|
|
3356
3963
|
const bodyMatch = /"retryDelay"\s*:\s*"(\d+)(?:\.\d+)?s"/i.exec(body);
|
|
3357
3964
|
if (bodyMatch) {
|
|
3358
3965
|
const sec = Number(bodyMatch[1]);
|
|
@@ -3371,16 +3978,16 @@ function headerGet(h, name) {
|
|
|
3371
3978
|
}
|
|
3372
3979
|
return null;
|
|
3373
3980
|
}
|
|
3374
|
-
function rateLimitMessage(
|
|
3375
|
-
const waitHint =
|
|
3981
|
+
function rateLimitMessage(err2) {
|
|
3982
|
+
const waitHint = err2.retryAfterSec ?? 60;
|
|
3376
3983
|
const limitsUrl = {
|
|
3377
3984
|
anthropic: "https://console.anthropic.com/settings/limits",
|
|
3378
3985
|
openai: "https://platform.openai.com/settings/organization/limits",
|
|
3379
3986
|
google: "https://aistudio.google.com/app/plan_information"
|
|
3380
3987
|
};
|
|
3381
|
-
const headline =
|
|
3382
|
-
const cap =
|
|
3383
|
-
const retried =
|
|
3988
|
+
const headline = err2.retryAfterSec ? `${err2.provider} rate limit hit \u2014 provider asked us to wait ${err2.retryAfterSec}s before retrying.` : `${err2.provider} rate limit hit.`;
|
|
3989
|
+
const cap = err2.maxRetrySec ?? 90;
|
|
3990
|
+
const retried = err2.retrySkippedReason === "retry_after_too_long" ? `squad-kit did not auto-retry: the provider's ${err2.retryAfterSec}s wait is longer than our ${cap}s cap, so retrying would just burn another request inside the same throttle window.` : err2.retryAlreadyAttempted ? "squad-kit already retried once automatically and was throttled again \u2014 your org is firmly over its per-minute quota." : "squad-kit aborted before retrying.";
|
|
3384
3991
|
return [
|
|
3385
3992
|
headline,
|
|
3386
3993
|
retried,
|
|
@@ -3389,427 +3996,148 @@ function rateLimitMessage(err) {
|
|
|
3389
3996
|
` 1. Wait ${waitHint}s and rerun \`squad new-plan --api\`. Per-minute limits reset quickly.`,
|
|
3390
3997
|
` 2. Switch to a smaller planner model: \`squad config set planner\` (pick a cheaper model id).`,
|
|
3391
3998
|
` 3. Tighten \`planner.budget\` in \`.squad/config.yaml\` (smaller \`maxContextBytes\` / \`maxFileReads\`) so each request carries fewer tokens.`,
|
|
3392
|
-
` 4. Upgrade your ${
|
|
3999
|
+
` 4. Upgrade your ${err2.provider} tier: ${limitsUrl[err2.provider]}.`,
|
|
3393
4000
|
"",
|
|
3394
4001
|
"Full runbook: docs/migrating-from-0.1.md (see \xA78. If something goes wrong).",
|
|
3395
4002
|
"",
|
|
3396
|
-
`Raw provider response: ${
|
|
4003
|
+
`Raw provider response: ${err2.rawBody.slice(0, 200)}`
|
|
3397
4004
|
].join("\n");
|
|
3398
4005
|
}
|
|
3399
4006
|
|
|
3400
|
-
// src/planner/
|
|
3401
|
-
function
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
4007
|
+
// src/planner/usage-map.ts
|
|
4008
|
+
function usageFromLanguageModelStep(usage, providerMetadata) {
|
|
4009
|
+
const out = {
|
|
4010
|
+
inputTokens: usage.promptTokens,
|
|
4011
|
+
outputTokens: usage.completionTokens
|
|
4012
|
+
};
|
|
4013
|
+
const meta = providerMetadata;
|
|
4014
|
+
const a = meta?.anthropic;
|
|
4015
|
+
if (a) {
|
|
4016
|
+
out.cacheCreationTokens = a.cacheCreationInputTokens ?? 0;
|
|
4017
|
+
out.cacheReadTokens = a.cacheReadInputTokens ?? 0;
|
|
3408
4018
|
}
|
|
3409
4019
|
return out;
|
|
3410
4020
|
}
|
|
3411
4021
|
|
|
3412
|
-
// src/planner/
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
system: [
|
|
3438
|
-
{
|
|
3439
|
-
type: "text",
|
|
3440
|
-
text: req.systemPrompt,
|
|
3441
|
-
cache_control: { type: "ephemeral" }
|
|
3442
|
-
}
|
|
3443
|
-
],
|
|
3444
|
-
tools: req.tools.map(toAnthropicTool)
|
|
3445
|
-
};
|
|
3446
|
-
if (req.turns.length === 0) {
|
|
3447
|
-
return base;
|
|
3448
|
-
}
|
|
3449
|
-
return { ...base, messages: toMessagesWithCacheMarkers(req.turns) };
|
|
3450
|
-
}
|
|
3451
|
-
async function callAnthropic(req) {
|
|
3452
|
-
const built = buildAnthropicBody(req);
|
|
3453
|
-
const body = { ...built, messages: built.messages ?? [] };
|
|
3454
|
-
const res = await fetch(API_URL, {
|
|
3455
|
-
method: "POST",
|
|
3456
|
-
headers: {
|
|
3457
|
-
"x-api-key": req.apiKey,
|
|
3458
|
-
"anthropic-version": API_VERSION,
|
|
3459
|
-
"content-type": "application/json"
|
|
3460
|
-
},
|
|
3461
|
-
body: JSON.stringify(body),
|
|
3462
|
-
signal: req.abort
|
|
3463
|
-
});
|
|
3464
|
-
if (!res.ok) {
|
|
3465
|
-
const text = await res.text().catch(() => "");
|
|
3466
|
-
const nf = detectModelNotFound("anthropic", req.model, res.status, text);
|
|
3467
|
-
if (nf) {
|
|
3468
|
-
return {
|
|
3469
|
-
stopReason: "error",
|
|
3470
|
-
rawError: modelNotFoundMessage(nf),
|
|
3471
|
-
errorKind: "model_not_found"
|
|
3472
|
-
};
|
|
3473
|
-
}
|
|
3474
|
-
const rl = detectRateLimit("anthropic", res.status, res.headers, text);
|
|
3475
|
-
if (rl) {
|
|
3476
|
-
return {
|
|
3477
|
-
stopReason: "error",
|
|
3478
|
-
rawError: `anthropic 429: ${text.slice(0, 500)}`,
|
|
3479
|
-
errorKind: "rate_limit",
|
|
3480
|
-
retryAfterSec: rl.retryAfterSec
|
|
3481
|
-
};
|
|
3482
|
-
}
|
|
3483
|
-
return { stopReason: "error", rawError: `anthropic ${res.status}: ${text.slice(0, 500)}` };
|
|
3484
|
-
}
|
|
3485
|
-
const json = await res.json();
|
|
3486
|
-
const textOut = json.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
3487
|
-
const toolCalls = json.content.filter((b) => b.type === "tool_use").map((b) => ({ id: b.id, name: b.name, input: b.input }));
|
|
3488
|
-
return {
|
|
3489
|
-
text: textOut || void 0,
|
|
3490
|
-
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
3491
|
-
stopReason: mapStop(json.stop_reason),
|
|
3492
|
-
usage: {
|
|
3493
|
-
inputTokens: json.usage?.input_tokens ?? 0,
|
|
3494
|
-
outputTokens: json.usage?.output_tokens ?? 0,
|
|
3495
|
-
cacheCreationTokens: json.usage?.cache_creation_input_tokens ?? 0,
|
|
3496
|
-
cacheReadTokens: json.usage?.cache_read_input_tokens ?? 0
|
|
3497
|
-
}
|
|
3498
|
-
};
|
|
3499
|
-
}
|
|
3500
|
-
function toMessagesWithCacheMarkers(turns) {
|
|
3501
|
-
let lastUserWithToolResultIdx = -1;
|
|
3502
|
-
for (let i = turns.length - 1; i >= 0; i -= 1) {
|
|
3503
|
-
const t = turns[i];
|
|
3504
|
-
if (t.role === "user" && hasToolResultBlock(t)) {
|
|
3505
|
-
lastUserWithToolResultIdx = i;
|
|
3506
|
-
break;
|
|
3507
|
-
}
|
|
3508
|
-
}
|
|
3509
|
-
return turns.map((turn, idx) => {
|
|
3510
|
-
const message = toAnthropicMessage(turn);
|
|
3511
|
-
if (idx === lastUserWithToolResultIdx) {
|
|
3512
|
-
return attachCacheControlToLastToolResult(message);
|
|
3513
|
-
}
|
|
3514
|
-
return message;
|
|
3515
|
-
});
|
|
3516
|
-
}
|
|
3517
|
-
function hasToolResultBlock(turn) {
|
|
3518
|
-
return (turn.toolResults?.length ?? 0) > 0;
|
|
3519
|
-
}
|
|
3520
|
-
function attachCacheControlToLastToolResult(msg) {
|
|
3521
|
-
const content = [...msg.content];
|
|
3522
|
-
for (let i = content.length - 1; i >= 0; i -= 1) {
|
|
3523
|
-
const block = content[i];
|
|
3524
|
-
if (block.type === "tool_result") {
|
|
3525
|
-
content[i] = { ...block, cache_control: { type: "ephemeral" } };
|
|
3526
|
-
break;
|
|
3527
|
-
}
|
|
3528
|
-
}
|
|
3529
|
-
return { ...msg, content };
|
|
4022
|
+
// src/planner/system-prompt.ts
|
|
4023
|
+
function composeSystemPrompt(args) {
|
|
4024
|
+
const base = readBundledPrompt("generate-plan.md").replace(/\{\{projectRoots\}\}/g, args.projectRoots.join(", ")).replace(/\{\{primaryLanguage\}\}/g, args.primaryLanguage).replace(/\{\{trackerType\}\}/g, args.trackerType).replace(
|
|
4025
|
+
/\{\{intakeContent\}\}/g,
|
|
4026
|
+
"_(The intake story is provided in the user message below. Do not ask for it.)_"
|
|
4027
|
+
);
|
|
4028
|
+
const apiPreamble = [
|
|
4029
|
+
"",
|
|
4030
|
+
"---",
|
|
4031
|
+
"",
|
|
4032
|
+
"## Direct-API mode notes",
|
|
4033
|
+
"",
|
|
4034
|
+
"You are being invoked by the squad-kit CLI, not by a human editor. The intake is already inlined below.",
|
|
4035
|
+
"The repository tree is provided. Tools: **`list_dir`** (one-level directory listing), **`grep`** (search), **`read_file`** (whole file up to 32 KB, or **`offset` + `limit`** for a line range \u2014 preferred when you know the region).",
|
|
4036
|
+
"You have a bounded context budget (tool calls, total bytes, wall-clock time). Prefer `grep` and ranged `read_file` over whole-file reads.",
|
|
4037
|
+
"When you have enough information, output the **complete plan markdown** as your final assistant message. No prose around it, no code fences wrapping the whole plan. The CLI will write your final message verbatim to disk.",
|
|
4038
|
+
"",
|
|
4039
|
+
"## Repository tree",
|
|
4040
|
+
"",
|
|
4041
|
+
"```",
|
|
4042
|
+
args.repoMap.trimEnd(),
|
|
4043
|
+
"```",
|
|
4044
|
+
""
|
|
4045
|
+
].join("\n");
|
|
4046
|
+
return base + apiPreamble;
|
|
3530
4047
|
}
|
|
3531
|
-
function
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
input_schema: sortRecordKeys(t.inputSchema)
|
|
3536
|
-
};
|
|
4048
|
+
function composeScoutSystemPrompt(args) {
|
|
4049
|
+
const plans = args.plansIndex?.trim() || "(no prior plans yet)";
|
|
4050
|
+
const base = readBundledPrompt("scout.md").replace(/\{\{projectRoots\}\}/g, args.projectRoots.join(", ")).replace(/\{\{primaryLanguage\}\}/g, args.primaryLanguage).replace(/\{\{trackerType\}\}/g, args.trackerType).replace(/\{\{repoMap\}\}/g, args.repoMap.trimEnd()).replace(/\{\{plansIndex\}\}/g, plans);
|
|
4051
|
+
return base;
|
|
3537
4052
|
}
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
name: tc.name,
|
|
3547
|
-
input: sortRecordKeys(tc.input)
|
|
3548
|
-
});
|
|
3549
|
-
}
|
|
3550
|
-
}
|
|
3551
|
-
if (turn.toolResults) {
|
|
3552
|
-
for (const tr of turn.toolResults) {
|
|
3553
|
-
blocks.push({
|
|
3554
|
-
type: "tool_result",
|
|
3555
|
-
tool_use_id: tr.toolCallId,
|
|
3556
|
-
content: tr.content,
|
|
3557
|
-
is_error: tr.isError ?? false
|
|
3558
|
-
});
|
|
3559
|
-
}
|
|
3560
|
-
}
|
|
3561
|
-
return { role: turn.role === "assistant" ? "assistant" : "user", content: blocks };
|
|
4053
|
+
var SCOUTED_USER_MESSAGE_CACHE_SENTINEL = "I have already scouted the project";
|
|
4054
|
+
function composeScoutedUserMessage(args) {
|
|
4055
|
+
return [
|
|
4056
|
+
SCOUTED_USER_MESSAGE_CACHE_SENTINEL + " for relevant files. Use this as your starting context; call `read_file`/`grep`/`list_dir` if you need to read more.",
|
|
4057
|
+
"",
|
|
4058
|
+
args.scoutedSection.trim(),
|
|
4059
|
+
""
|
|
4060
|
+
].join("\n");
|
|
3562
4061
|
}
|
|
3563
|
-
function
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
4062
|
+
function composeUserPrompt(args) {
|
|
4063
|
+
return [
|
|
4064
|
+
"Produce the implementation plan for the following intake.",
|
|
4065
|
+
"",
|
|
4066
|
+
"---",
|
|
4067
|
+
"",
|
|
4068
|
+
args.intakeContent.trimEnd(),
|
|
4069
|
+
""
|
|
4070
|
+
].join("\n");
|
|
3567
4071
|
}
|
|
3568
4072
|
|
|
3569
|
-
// src/planner/
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
}
|
|
3577
|
-
function buildOpenAIBody(req) {
|
|
3578
|
-
const base = {
|
|
3579
|
-
model: req.model,
|
|
3580
|
-
max_completion_tokens: req.maxOutputTokens ?? 4096,
|
|
3581
|
-
tools: req.tools.map(toOpenAITool)
|
|
3582
|
-
};
|
|
3583
|
-
if (req.turns.length === 0) {
|
|
3584
|
-
return base;
|
|
3585
|
-
}
|
|
3586
|
-
const messages = [{ role: "system", content: req.systemPrompt }];
|
|
3587
|
-
for (const turn of req.turns) messages.push(...toOpenAIMessages(turn));
|
|
3588
|
-
return { ...base, messages };
|
|
3589
|
-
}
|
|
3590
|
-
async function callOpenAI(req) {
|
|
3591
|
-
const built = buildOpenAIBody(req);
|
|
3592
|
-
const messages = built.messages && built.messages.length > 0 ? built.messages : [{ role: "system", content: req.systemPrompt }];
|
|
3593
|
-
const body = { ...built, messages };
|
|
3594
|
-
const res = await fetch(API_URL2, {
|
|
3595
|
-
method: "POST",
|
|
3596
|
-
headers: {
|
|
3597
|
-
Authorization: `Bearer ${req.apiKey}`,
|
|
3598
|
-
"content-type": "application/json"
|
|
3599
|
-
},
|
|
3600
|
-
body: JSON.stringify(body),
|
|
3601
|
-
signal: req.abort
|
|
3602
|
-
});
|
|
3603
|
-
if (!res.ok) {
|
|
3604
|
-
const text = await res.text().catch(() => "");
|
|
3605
|
-
const nf = detectModelNotFound("openai", req.model, res.status, text);
|
|
3606
|
-
if (nf) {
|
|
3607
|
-
return {
|
|
3608
|
-
stopReason: "error",
|
|
3609
|
-
rawError: modelNotFoundMessage(nf),
|
|
3610
|
-
errorKind: "model_not_found"
|
|
3611
|
-
};
|
|
3612
|
-
}
|
|
3613
|
-
const rl = detectRateLimit("openai", res.status, res.headers, text);
|
|
3614
|
-
if (rl) {
|
|
3615
|
-
return {
|
|
3616
|
-
stopReason: "error",
|
|
3617
|
-
rawError: `openai 429: ${text.slice(0, 500)}`,
|
|
3618
|
-
errorKind: "rate_limit",
|
|
3619
|
-
retryAfterSec: rl.retryAfterSec
|
|
3620
|
-
};
|
|
3621
|
-
}
|
|
3622
|
-
return { stopReason: "error", rawError: `openai ${res.status}: ${text.slice(0, 500)}` };
|
|
3623
|
-
}
|
|
3624
|
-
const json = await res.json();
|
|
3625
|
-
const choice = json.choices?.[0];
|
|
3626
|
-
if (!choice) return { stopReason: "error", rawError: "openai: empty choices" };
|
|
3627
|
-
const textOut = choice.message.content ?? "";
|
|
3628
|
-
const toolCalls = (choice.message.tool_calls ?? []).map((tc) => ({
|
|
3629
|
-
id: tc.id,
|
|
3630
|
-
name: tc.function.name,
|
|
3631
|
-
input: safeJson(tc.function.arguments)
|
|
3632
|
-
}));
|
|
3633
|
-
return {
|
|
3634
|
-
text: textOut || void 0,
|
|
3635
|
-
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
3636
|
-
stopReason: mapStop2(choice.finish_reason),
|
|
3637
|
-
usage: {
|
|
3638
|
-
inputTokens: json.usage?.prompt_tokens ?? 0,
|
|
3639
|
-
outputTokens: json.usage?.completion_tokens ?? 0
|
|
3640
|
-
}
|
|
3641
|
-
};
|
|
3642
|
-
}
|
|
3643
|
-
function toOpenAITool(t) {
|
|
3644
|
-
return {
|
|
3645
|
-
type: "function",
|
|
3646
|
-
function: {
|
|
3647
|
-
name: t.name,
|
|
3648
|
-
description: t.description,
|
|
3649
|
-
parameters: sortRecordKeys(t.inputSchema)
|
|
4073
|
+
// src/planner/core-messages.ts
|
|
4074
|
+
function turnsToCoreMessages(systemPrompt, turns, opts) {
|
|
4075
|
+
const { cacheEnabled, provider } = opts;
|
|
4076
|
+
const messages = [
|
|
4077
|
+
{
|
|
4078
|
+
role: "system",
|
|
4079
|
+
content: systemPrompt,
|
|
4080
|
+
...cacheEnabled && provider === "anthropic" ? { providerOptions: { anthropic: { cacheControl: { type: "ephemeral" } } } } : {}
|
|
3650
4081
|
}
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
type: "function",
|
|
3662
|
-
function: {
|
|
3663
|
-
name: tc.name,
|
|
3664
|
-
arguments: JSON.stringify(sortRecordKeys(tc.input))
|
|
4082
|
+
];
|
|
4083
|
+
let lastToolMessageIndex = -1;
|
|
4084
|
+
for (const t of turns) {
|
|
4085
|
+
if (t.role === "user") {
|
|
4086
|
+
if (t.text !== void 0 && t.text.length > 0) {
|
|
4087
|
+
const userMsg = { role: "user", content: t.text };
|
|
4088
|
+
if (cacheEnabled && provider === "anthropic" && t.text.startsWith(SCOUTED_USER_MESSAGE_CACHE_SENTINEL)) {
|
|
4089
|
+
userMsg.providerOptions = {
|
|
4090
|
+
anthropic: { cacheControl: { type: "ephemeral" } }
|
|
4091
|
+
};
|
|
3665
4092
|
}
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
4093
|
+
messages.push(userMsg);
|
|
4094
|
+
}
|
|
4095
|
+
if (t.toolResults?.length) {
|
|
4096
|
+
const msg = {
|
|
4097
|
+
role: "tool",
|
|
4098
|
+
content: t.toolResults.map((tr) => ({
|
|
4099
|
+
type: "tool-result",
|
|
4100
|
+
toolCallId: tr.toolCallId,
|
|
4101
|
+
toolName: tr.name,
|
|
4102
|
+
result: tr.content,
|
|
4103
|
+
...tr.isError ? { isError: true } : {}
|
|
4104
|
+
}))
|
|
4105
|
+
};
|
|
4106
|
+
lastToolMessageIndex = messages.push(msg) - 1;
|
|
4107
|
+
}
|
|
4108
|
+
} else {
|
|
4109
|
+
const parts = [];
|
|
4110
|
+
if (t.text) parts.push({ type: "text", text: t.text });
|
|
4111
|
+
for (const tc of t.toolCalls ?? []) {
|
|
4112
|
+
parts.push({
|
|
4113
|
+
type: "tool-call",
|
|
4114
|
+
toolCallId: tc.id,
|
|
4115
|
+
toolName: tc.name,
|
|
4116
|
+
args: tc.input
|
|
4117
|
+
});
|
|
4118
|
+
}
|
|
4119
|
+
if (parts.length === 0) continue;
|
|
4120
|
+
if (parts.length === 1 && parts[0].type === "text") {
|
|
4121
|
+
messages.push({ role: "assistant", content: parts[0].text });
|
|
4122
|
+
} else {
|
|
4123
|
+
messages.push({ role: "assistant", content: parts });
|
|
3673
4124
|
}
|
|
3674
4125
|
}
|
|
3675
4126
|
}
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
return "end_turn";
|
|
3682
|
-
}
|
|
3683
|
-
function safeJson(raw) {
|
|
3684
|
-
try {
|
|
3685
|
-
return sortRecordKeys(JSON.parse(raw));
|
|
3686
|
-
} catch {
|
|
3687
|
-
return {};
|
|
3688
|
-
}
|
|
3689
|
-
}
|
|
3690
|
-
|
|
3691
|
-
// src/planner/providers/google.ts
|
|
3692
|
-
var API_BASE = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
3693
|
-
var googleProvider = {
|
|
3694
|
-
name: "google",
|
|
3695
|
-
async send(req) {
|
|
3696
|
-
return callGoogle(req);
|
|
3697
|
-
}
|
|
3698
|
-
};
|
|
3699
|
-
function buildGoogleBody(req) {
|
|
3700
|
-
const base = {
|
|
3701
|
-
system_instruction: { parts: [{ text: req.systemPrompt }] },
|
|
3702
|
-
generationConfig: { maxOutputTokens: req.maxOutputTokens ?? 4096 }
|
|
3703
|
-
};
|
|
3704
|
-
if (req.tools.length) {
|
|
3705
|
-
base.tools = [{ functionDeclarations: req.tools.map(toGoogleTool) }];
|
|
3706
|
-
}
|
|
3707
|
-
if (req.turns.length === 0) {
|
|
3708
|
-
return base;
|
|
3709
|
-
}
|
|
3710
|
-
return { ...base, contents: req.turns.map(toGoogleContent) };
|
|
3711
|
-
}
|
|
3712
|
-
async function callGoogle(req) {
|
|
3713
|
-
const url = `${API_BASE}/${encodeURIComponent(req.model)}:generateContent?key=${encodeURIComponent(req.apiKey)}`;
|
|
3714
|
-
const built = buildGoogleBody(req);
|
|
3715
|
-
const body = { ...built, contents: built.contents ?? [] };
|
|
3716
|
-
const res = await fetch(url, {
|
|
3717
|
-
method: "POST",
|
|
3718
|
-
headers: { "content-type": "application/json" },
|
|
3719
|
-
body: JSON.stringify(body),
|
|
3720
|
-
signal: req.abort
|
|
3721
|
-
});
|
|
3722
|
-
if (!res.ok) {
|
|
3723
|
-
const text = await res.text().catch(() => "");
|
|
3724
|
-
const nf = detectModelNotFound("google", req.model, res.status, text);
|
|
3725
|
-
if (nf) {
|
|
3726
|
-
return {
|
|
3727
|
-
stopReason: "error",
|
|
3728
|
-
rawError: modelNotFoundMessage(nf),
|
|
3729
|
-
errorKind: "model_not_found"
|
|
4127
|
+
if (cacheEnabled && provider === "anthropic" && lastToolMessageIndex >= 0) {
|
|
4128
|
+
const target = messages[lastToolMessageIndex];
|
|
4129
|
+
if (target.role === "tool") {
|
|
4130
|
+
target.providerOptions = {
|
|
4131
|
+
anthropic: { cacheControl: { type: "ephemeral" } }
|
|
3730
4132
|
};
|
|
3731
4133
|
}
|
|
3732
|
-
const rl = detectRateLimit("google", res.status, res.headers, text);
|
|
3733
|
-
if (rl) {
|
|
3734
|
-
return {
|
|
3735
|
-
stopReason: "error",
|
|
3736
|
-
rawError: `google 429: ${text.slice(0, 500)}`,
|
|
3737
|
-
errorKind: "rate_limit",
|
|
3738
|
-
retryAfterSec: rl.retryAfterSec
|
|
3739
|
-
};
|
|
3740
|
-
}
|
|
3741
|
-
return { stopReason: "error", rawError: `google ${res.status}: ${text.slice(0, 500)}` };
|
|
3742
|
-
}
|
|
3743
|
-
const json = await res.json();
|
|
3744
|
-
const cand = json.candidates?.[0];
|
|
3745
|
-
if (!cand) return { stopReason: "error", rawError: "google: empty candidates" };
|
|
3746
|
-
const parts = cand.content?.parts ?? [];
|
|
3747
|
-
const textOut = parts.filter((p) => "text" in p && !!p.text).map((p) => p.text).join("");
|
|
3748
|
-
const toolCalls = parts.filter((p) => "functionCall" in p && !!p.functionCall).map((p, i) => ({
|
|
3749
|
-
id: `gcall_${i}_${p.functionCall.name}`,
|
|
3750
|
-
name: p.functionCall.name,
|
|
3751
|
-
input: p.functionCall.args ? sortRecordKeys(p.functionCall.args) : {}
|
|
3752
|
-
}));
|
|
3753
|
-
return {
|
|
3754
|
-
text: textOut || void 0,
|
|
3755
|
-
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
3756
|
-
stopReason: mapStop3(cand.finishReason),
|
|
3757
|
-
usage: {
|
|
3758
|
-
inputTokens: json.usageMetadata?.promptTokenCount ?? 0,
|
|
3759
|
-
outputTokens: json.usageMetadata?.candidatesTokenCount ?? 0
|
|
3760
|
-
}
|
|
3761
|
-
};
|
|
3762
|
-
}
|
|
3763
|
-
function toGoogleTool(t) {
|
|
3764
|
-
return { name: t.name, description: t.description, parameters: sortRecordKeys(t.inputSchema) };
|
|
3765
|
-
}
|
|
3766
|
-
function toGoogleContent(turn) {
|
|
3767
|
-
const role = turn.role === "assistant" ? "model" : "user";
|
|
3768
|
-
const parts = [];
|
|
3769
|
-
if (turn.text) parts.push({ text: turn.text });
|
|
3770
|
-
if (turn.toolCalls) {
|
|
3771
|
-
for (const tc of turn.toolCalls) {
|
|
3772
|
-
parts.push({ functionCall: { name: tc.name, args: sortRecordKeys(tc.input) } });
|
|
3773
|
-
}
|
|
3774
4134
|
}
|
|
3775
|
-
|
|
3776
|
-
for (const tr of turn.toolResults) {
|
|
3777
|
-
parts.push({
|
|
3778
|
-
functionResponse: { name: nameFromToolCallId(tr.toolCallId), response: { content: tr.content } }
|
|
3779
|
-
});
|
|
3780
|
-
}
|
|
3781
|
-
}
|
|
3782
|
-
return { role, parts };
|
|
3783
|
-
}
|
|
3784
|
-
function nameFromToolCallId(id) {
|
|
3785
|
-
const m = id.match(/^gcall_\d+_(.+)$/);
|
|
3786
|
-
return m?.[1] ?? "read_file";
|
|
3787
|
-
}
|
|
3788
|
-
function mapStop3(r) {
|
|
3789
|
-
if (r === "STOP" || r === "END_OF_TURN" || r === "FINISH_REASON_STOP") return "end_turn";
|
|
3790
|
-
if (r === "MAX_TOKENS") return "max_tokens";
|
|
3791
|
-
return "tool_use";
|
|
3792
|
-
}
|
|
3793
|
-
|
|
3794
|
-
// src/planner/providers/prefix.ts
|
|
3795
|
-
function prefixOf(provider, req) {
|
|
3796
|
-
const trimmed = { ...req, turns: req.turns.slice(0, -1) };
|
|
3797
|
-
const s = (() => {
|
|
3798
|
-
switch (provider) {
|
|
3799
|
-
case "anthropic":
|
|
3800
|
-
return JSON.stringify(buildAnthropicBody(trimmed));
|
|
3801
|
-
case "openai":
|
|
3802
|
-
return JSON.stringify(buildOpenAIBody(trimmed));
|
|
3803
|
-
case "google":
|
|
3804
|
-
return JSON.stringify(buildGoogleBody(trimmed));
|
|
3805
|
-
}
|
|
3806
|
-
})();
|
|
3807
|
-
return s.length > 0 ? s.slice(0, -1) : s;
|
|
4135
|
+
return messages;
|
|
3808
4136
|
}
|
|
3809
4137
|
|
|
3810
4138
|
// src/core/runs.ts
|
|
3811
|
-
import
|
|
3812
|
-
import
|
|
4139
|
+
import fs25 from "fs/promises";
|
|
4140
|
+
import path17 from "path";
|
|
3813
4141
|
import { randomBytes } from "crypto";
|
|
3814
4142
|
var RING_SIZE = 20;
|
|
3815
4143
|
function newRunId() {
|
|
@@ -3818,24 +4146,24 @@ function newRunId() {
|
|
|
3818
4146
|
return `${ts}-${rnd}`;
|
|
3819
4147
|
}
|
|
3820
4148
|
async function appendRun(paths, rec) {
|
|
3821
|
-
const dir =
|
|
3822
|
-
await
|
|
3823
|
-
const file =
|
|
3824
|
-
await
|
|
4149
|
+
const dir = path17.join(paths.squadDir, "runs");
|
|
4150
|
+
await fs25.mkdir(dir, { recursive: true });
|
|
4151
|
+
const file = path17.join(dir, `${rec.runId}.json`);
|
|
4152
|
+
await fs25.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
|
|
3825
4153
|
await pruneOldRuns(dir);
|
|
3826
4154
|
}
|
|
3827
4155
|
async function pruneOldRuns(dir) {
|
|
3828
|
-
const entries = (await
|
|
4156
|
+
const entries = (await fs25.readdir(dir)).filter((n) => n.endsWith(".json"));
|
|
3829
4157
|
if (entries.length <= RING_SIZE) return;
|
|
3830
4158
|
entries.sort();
|
|
3831
4159
|
const drop = entries.slice(0, entries.length - RING_SIZE);
|
|
3832
|
-
await Promise.all(drop.map((n) =>
|
|
4160
|
+
await Promise.all(drop.map((n) => fs25.rm(path17.join(dir, n), { force: true })));
|
|
3833
4161
|
}
|
|
3834
4162
|
async function listRuns(paths) {
|
|
3835
|
-
const dir =
|
|
4163
|
+
const dir = path17.join(paths.squadDir, "runs");
|
|
3836
4164
|
let entries;
|
|
3837
4165
|
try {
|
|
3838
|
-
entries = await
|
|
4166
|
+
entries = await fs25.readdir(dir);
|
|
3839
4167
|
} catch {
|
|
3840
4168
|
return [];
|
|
3841
4169
|
}
|
|
@@ -3843,7 +4171,7 @@ async function listRuns(paths) {
|
|
|
3843
4171
|
const out = [];
|
|
3844
4172
|
for (const f of files) {
|
|
3845
4173
|
try {
|
|
3846
|
-
const raw = await
|
|
4174
|
+
const raw = await fs25.readFile(path17.join(dir, f), "utf8");
|
|
3847
4175
|
const parsed = JSON.parse(raw);
|
|
3848
4176
|
if (parsed.version === 1) out.push(parsed);
|
|
3849
4177
|
} catch {
|
|
@@ -3873,6 +4201,355 @@ var PlannerEventBus = class {
|
|
|
3873
4201
|
}
|
|
3874
4202
|
};
|
|
3875
4203
|
|
|
4204
|
+
// src/planner/stages/scout.ts
|
|
4205
|
+
import { APICallError, generateObject, NoObjectGeneratedError } from "ai";
|
|
4206
|
+
import { z as z4 } from "zod";
|
|
4207
|
+
var ScoutOutputSchema = z4.object({
|
|
4208
|
+
selectedFiles: z4.array(z4.string()).min(1).max(25),
|
|
4209
|
+
reasoning: z4.string().min(1),
|
|
4210
|
+
suggestedReadStrategy: z4.enum(["read_full", "read_ranges", "mixed"]),
|
|
4211
|
+
readRanges: z4.array(
|
|
4212
|
+
z4.object({
|
|
4213
|
+
path: z4.string(),
|
|
4214
|
+
offset: z4.number().int().min(1),
|
|
4215
|
+
limit: z4.number().int().min(1).max(400)
|
|
4216
|
+
})
|
|
4217
|
+
).optional()
|
|
4218
|
+
});
|
|
4219
|
+
async function runScout(input5) {
|
|
4220
|
+
const maxTokens = input5.maxTokens ?? 2048;
|
|
4221
|
+
let attempt = 0;
|
|
4222
|
+
while (true) {
|
|
4223
|
+
try {
|
|
4224
|
+
const { object, usage, providerMetadata } = await generateObject({
|
|
4225
|
+
model: input5.model,
|
|
4226
|
+
system: input5.systemPrompt,
|
|
4227
|
+
prompt: input5.userPrompt,
|
|
4228
|
+
schema: ScoutOutputSchema,
|
|
4229
|
+
maxTokens,
|
|
4230
|
+
abortSignal: input5.abort
|
|
4231
|
+
});
|
|
4232
|
+
const u = usageFromLanguageModelStep(usage, providerMetadata);
|
|
4233
|
+
input5.budget.recordUsage(u);
|
|
4234
|
+
return {
|
|
4235
|
+
output: object,
|
|
4236
|
+
usage: { inputTokens: u.inputTokens, outputTokens: u.outputTokens }
|
|
4237
|
+
};
|
|
4238
|
+
} catch (e) {
|
|
4239
|
+
if (NoObjectGeneratedError.isInstance(e)) {
|
|
4240
|
+
return null;
|
|
4241
|
+
}
|
|
4242
|
+
if (APICallError.isInstance(e) && attempt === 0) {
|
|
4243
|
+
const status = e.statusCode ?? 0;
|
|
4244
|
+
if (status === 429) {
|
|
4245
|
+
const ra = parseRetryAfterSec(e.responseHeaders ?? {});
|
|
4246
|
+
if (ra !== void 0 && ra <= 90) {
|
|
4247
|
+
await new Promise((r) => setTimeout(r, ra * 1e3));
|
|
4248
|
+
attempt += 1;
|
|
4249
|
+
continue;
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
throw e;
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
// src/planner/validation.ts
|
|
4259
|
+
import fs26 from "fs";
|
|
4260
|
+
import path18 from "path";
|
|
4261
|
+
var CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|md|json|ya?ml|css|html|py|go|rs|cs|java|kt|kts|swift|rb|php|vue|svelte)$/i;
|
|
4262
|
+
function summariseIssuesByKind(issues) {
|
|
4263
|
+
const out = {};
|
|
4264
|
+
for (const i of issues) {
|
|
4265
|
+
out[i.kind] = (out[i.kind] ?? 0) + 1;
|
|
4266
|
+
}
|
|
4267
|
+
return out;
|
|
4268
|
+
}
|
|
4269
|
+
function extractBacktickPaths(planText) {
|
|
4270
|
+
const claims = [];
|
|
4271
|
+
const re = /`([^`\n]+)`/g;
|
|
4272
|
+
let m;
|
|
4273
|
+
while ((m = re.exec(planText)) !== null) {
|
|
4274
|
+
const inner = m[1].trim();
|
|
4275
|
+
if (!inner.includes("/") || inner.startsWith("http")) continue;
|
|
4276
|
+
if (inner.includes("story ") || inner.includes("Story ")) continue;
|
|
4277
|
+
const seg = inner.split(/[\s:]/)[0];
|
|
4278
|
+
if (!seg || !CODE_EXT.test(seg)) continue;
|
|
4279
|
+
claims.push({ path: seg.replace(/^\.\//, ""), isCreate: false, excerpt: m[0] });
|
|
4280
|
+
}
|
|
4281
|
+
return claims;
|
|
4282
|
+
}
|
|
4283
|
+
function extractFileHeadingPaths(planText) {
|
|
4284
|
+
const claims = [];
|
|
4285
|
+
const fileRe = /\*\*\s*(?:File|Create file)\s*:\s*`([^`\n]+)`\s*\*\*/gi;
|
|
4286
|
+
let m;
|
|
4287
|
+
while ((m = fileRe.exec(planText)) !== null) {
|
|
4288
|
+
const p = m[1].trim().replace(/^\.\//, "");
|
|
4289
|
+
const isCreate = /create file/i.test(m[0]);
|
|
4290
|
+
claims.push({ path: p, isCreate, excerpt: m[0].slice(0, 120) });
|
|
4291
|
+
}
|
|
4292
|
+
return claims;
|
|
4293
|
+
}
|
|
4294
|
+
function mergePathClaims(planText) {
|
|
4295
|
+
const map = /* @__PURE__ */ new Map();
|
|
4296
|
+
for (const c of [...extractFileHeadingPaths(planText), ...extractBacktickPaths(planText)]) {
|
|
4297
|
+
const key = `${c.isCreate}:${c.path}`;
|
|
4298
|
+
if (!map.has(key)) map.set(key, c);
|
|
4299
|
+
}
|
|
4300
|
+
return [...map.values()];
|
|
4301
|
+
}
|
|
4302
|
+
var LINE_RANGE_RE = /(?:~?\s*lines?\s*|lines\s+)(\d+)\s*[–-]\s*(\d+)/gi;
|
|
4303
|
+
var LINE_SINGLE_WORD_RE = /\bline\s+(\d+)\b|\(L(\d+)\)/gi;
|
|
4304
|
+
var PATH_COLON_LINE_RE = /`([^`\n]+\.[a-z0-9]{1,6})`\s*:\s*(\d+)\b/gi;
|
|
4305
|
+
function pathNear(planText, idx) {
|
|
4306
|
+
const before = planText.slice(Math.max(0, idx - 200), idx);
|
|
4307
|
+
const pathMatch = before.match(/`([^`\n]+\.[a-z0-9]{1,6})`/i);
|
|
4308
|
+
const filePath = pathMatch?.[1]?.trim().replace(/^\.\//, "");
|
|
4309
|
+
return filePath;
|
|
4310
|
+
}
|
|
4311
|
+
function trigrams(s) {
|
|
4312
|
+
const x = s.toLowerCase();
|
|
4313
|
+
const g = /* @__PURE__ */ new Set();
|
|
4314
|
+
for (let i = 0; i + 3 <= x.length; i++) {
|
|
4315
|
+
g.add(x.slice(i, i + 3));
|
|
4316
|
+
}
|
|
4317
|
+
return g;
|
|
4318
|
+
}
|
|
4319
|
+
function trigramSimilarity(a, b) {
|
|
4320
|
+
const A = trigrams(a);
|
|
4321
|
+
const B = trigrams(b);
|
|
4322
|
+
if (A.size === 0 || B.size === 0) return 0;
|
|
4323
|
+
let inter = 0;
|
|
4324
|
+
for (const t of A) {
|
|
4325
|
+
if (B.has(t)) inter += 1;
|
|
4326
|
+
}
|
|
4327
|
+
return inter / Math.sqrt(A.size * B.size);
|
|
4328
|
+
}
|
|
4329
|
+
function suggestPaths(root, missingRel, max = 3) {
|
|
4330
|
+
const targetBase = path18.basename(missingRel).toLowerCase();
|
|
4331
|
+
const ig = buildRepoIgnore(root);
|
|
4332
|
+
const candidates = [];
|
|
4333
|
+
const walk2 = (dir) => {
|
|
4334
|
+
if (candidates.length >= 2e3) return;
|
|
4335
|
+
let ents;
|
|
4336
|
+
try {
|
|
4337
|
+
ents = fs26.readdirSync(dir, { withFileTypes: true });
|
|
4338
|
+
} catch {
|
|
4339
|
+
return;
|
|
4340
|
+
}
|
|
4341
|
+
for (const ent of ents) {
|
|
4342
|
+
if (candidates.length >= 2e3) return;
|
|
4343
|
+
const abs = path18.join(dir, ent.name);
|
|
4344
|
+
const rel = path18.relative(root, abs).split(path18.sep).join("/");
|
|
4345
|
+
if (!rel || ig.ignores(rel) || ent.isDirectory() && ig.ignores(`${rel}/`)) continue;
|
|
4346
|
+
if (ent.isDirectory()) walk2(abs);
|
|
4347
|
+
else if (ent.isFile()) candidates.push(rel);
|
|
4348
|
+
}
|
|
4349
|
+
};
|
|
4350
|
+
walk2(root);
|
|
4351
|
+
const rows = [];
|
|
4352
|
+
for (const rel of candidates) {
|
|
4353
|
+
const base = path18.basename(rel).toLowerCase();
|
|
4354
|
+
let score = 0;
|
|
4355
|
+
if (base === targetBase) score = 1e3;
|
|
4356
|
+
else if (base.startsWith(targetBase) || targetBase.startsWith(base)) score = 500;
|
|
4357
|
+
else if (base.includes(targetBase) || targetBase.includes(base)) score = 200;
|
|
4358
|
+
score += trigramSimilarity(targetBase, base) * 50;
|
|
4359
|
+
if (score > 0) rows.push({ rel, score });
|
|
4360
|
+
}
|
|
4361
|
+
rows.sort((a, b) => b.score - a.score || a.rel.localeCompare(b.rel));
|
|
4362
|
+
const uniq = [];
|
|
4363
|
+
for (const r of rows) {
|
|
4364
|
+
if (r.rel === missingRel.replace(/^\.\//, "")) continue;
|
|
4365
|
+
if (!uniq.includes(r.rel)) uniq.push(r.rel);
|
|
4366
|
+
if (uniq.length >= max) break;
|
|
4367
|
+
}
|
|
4368
|
+
return uniq;
|
|
4369
|
+
}
|
|
4370
|
+
var SYMBOL_DEF_RE = /(?:^|\n)\s*(?:export\s+)?(?:async\s+)?(?:function|class|interface|type|const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
4371
|
+
function collectSymbols(src, maxLines = 200) {
|
|
4372
|
+
const head = src.split(/\r?\n/).slice(0, maxLines).join("\n");
|
|
4373
|
+
const syms = /* @__PURE__ */ new Set();
|
|
4374
|
+
SYMBOL_DEF_RE.lastIndex = 0;
|
|
4375
|
+
let m;
|
|
4376
|
+
while ((m = SYMBOL_DEF_RE.exec(head)) !== null) {
|
|
4377
|
+
syms.add(m[1]);
|
|
4378
|
+
}
|
|
4379
|
+
return [...syms];
|
|
4380
|
+
}
|
|
4381
|
+
function leadingCharScore(sym, missing) {
|
|
4382
|
+
let i = 0;
|
|
4383
|
+
const a = sym.toLowerCase();
|
|
4384
|
+
const b = missing.toLowerCase();
|
|
4385
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
4386
|
+
return i * 10 + trigramSimilarity(sym, missing) * 30;
|
|
4387
|
+
}
|
|
4388
|
+
function suggestSymbols(fileText, missingSym, max = 3) {
|
|
4389
|
+
const defs = collectSymbols(fileText);
|
|
4390
|
+
const ranked = defs.filter((s) => s !== missingSym).map((s) => ({ s, score: leadingCharScore(s, missingSym) })).filter((r) => r.score > 0).sort((a, b) => b.score - a.score || a.s.localeCompare(b.s));
|
|
4391
|
+
const out = [];
|
|
4392
|
+
for (const r of ranked) {
|
|
4393
|
+
if (!out.includes(r.s)) out.push(r.s);
|
|
4394
|
+
if (out.length >= max) break;
|
|
4395
|
+
}
|
|
4396
|
+
return out;
|
|
4397
|
+
}
|
|
4398
|
+
function parseMetadataLine(planText) {
|
|
4399
|
+
const lines = planText.split(/\r?\n/);
|
|
4400
|
+
for (let i = 0; i < Math.min(5, lines.length); i++) {
|
|
4401
|
+
const line2 = (lines[i] ?? "").trim();
|
|
4402
|
+
if (!line2.startsWith("<!--") || !line2.includes("squad-kit:")) continue;
|
|
4403
|
+
if (!line2.endsWith("-->")) {
|
|
4404
|
+
return {
|
|
4405
|
+
severity: "warning",
|
|
4406
|
+
kind: "malformed_metadata",
|
|
4407
|
+
detail: "Metadata comment is not a single-line <!-- squad-kit: \u2026 -->",
|
|
4408
|
+
excerpt: line2.slice(0, 120)
|
|
4409
|
+
};
|
|
4410
|
+
}
|
|
4411
|
+
const inner = line2.replace(/^<!--\s*squad-kit:\s*/i, "").replace(/\s*-->$/, "");
|
|
4412
|
+
if (!inner.trim()) {
|
|
4413
|
+
return {
|
|
4414
|
+
severity: "warning",
|
|
4415
|
+
kind: "malformed_metadata",
|
|
4416
|
+
detail: "Empty squad-kit metadata body",
|
|
4417
|
+
excerpt: line2
|
|
4418
|
+
};
|
|
4419
|
+
}
|
|
4420
|
+
return void 0;
|
|
4421
|
+
}
|
|
4422
|
+
return void 0;
|
|
4423
|
+
}
|
|
4424
|
+
function validatePlan(input5) {
|
|
4425
|
+
const issues = [];
|
|
4426
|
+
const metaIssue = parseMetadataLine(input5.planText);
|
|
4427
|
+
if (metaIssue) issues.push(metaIssue);
|
|
4428
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4429
|
+
const readCached = (rel) => {
|
|
4430
|
+
if (cache.has(rel)) return cache.get(rel);
|
|
4431
|
+
const resolved = path18.resolve(input5.root, rel);
|
|
4432
|
+
const rc = path18.relative(input5.root, resolved);
|
|
4433
|
+
if (rc.startsWith("..") || path18.isAbsolute(rc)) return void 0;
|
|
4434
|
+
if (!fs26.existsSync(resolved) || !fs26.statSync(resolved).isFile()) return void 0;
|
|
4435
|
+
try {
|
|
4436
|
+
const buf = fs26.readFileSync(resolved);
|
|
4437
|
+
const isBinary = buf.includes(0);
|
|
4438
|
+
const text = isBinary ? "" : buf.toString("utf8");
|
|
4439
|
+
const lines = isBinary ? 0 : text.split(/\r?\n/).length;
|
|
4440
|
+
const entry = { text, lines, isBinary };
|
|
4441
|
+
cache.set(rel, entry);
|
|
4442
|
+
return entry;
|
|
4443
|
+
} catch {
|
|
4444
|
+
return void 0;
|
|
4445
|
+
}
|
|
4446
|
+
};
|
|
4447
|
+
const pathClaims = mergePathClaims(input5.planText);
|
|
4448
|
+
for (const c of pathClaims) {
|
|
4449
|
+
if (c.isCreate) continue;
|
|
4450
|
+
const resolved = path18.resolve(input5.root, c.path);
|
|
4451
|
+
const rel = path18.relative(input5.root, resolved);
|
|
4452
|
+
if (rel.startsWith("..") || path18.isAbsolute(rel)) continue;
|
|
4453
|
+
if (!fs26.existsSync(resolved)) {
|
|
4454
|
+
const suggestions = suggestPaths(input5.root, c.path);
|
|
4455
|
+
issues.push({
|
|
4456
|
+
severity: "warning",
|
|
4457
|
+
kind: "missing_path",
|
|
4458
|
+
path: c.path,
|
|
4459
|
+
detail: "Path cited in plan does not exist in the workspace" + (suggestions.length ? ` \u2014 did you mean: ${suggestions.join(", ")}?` : ""),
|
|
4460
|
+
excerpt: c.excerpt
|
|
4461
|
+
});
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
const rangeText = input5.planText;
|
|
4465
|
+
LINE_RANGE_RE.lastIndex = 0;
|
|
4466
|
+
let rm2;
|
|
4467
|
+
while ((rm2 = LINE_RANGE_RE.exec(rangeText)) !== null) {
|
|
4468
|
+
const lo = parseInt(rm2[1], 10);
|
|
4469
|
+
const hi = parseInt(rm2[2], 10);
|
|
4470
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi)) continue;
|
|
4471
|
+
const filePath = pathNear(rangeText, rm2.index);
|
|
4472
|
+
if (!filePath) continue;
|
|
4473
|
+
const cached = readCached(filePath);
|
|
4474
|
+
const lines = cached?.isBinary ? void 0 : cached?.lines;
|
|
4475
|
+
if (lines !== void 0 && hi > lines) {
|
|
4476
|
+
issues.push({
|
|
4477
|
+
severity: "warning",
|
|
4478
|
+
kind: "line_range_too_large",
|
|
4479
|
+
path: filePath,
|
|
4480
|
+
detail: `Range ends at line ${hi} but file has ${lines} lines`,
|
|
4481
|
+
excerpt: rm2[0].slice(0, 80)
|
|
4482
|
+
});
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
LINE_SINGLE_WORD_RE.lastIndex = 0;
|
|
4486
|
+
while ((rm2 = LINE_SINGLE_WORD_RE.exec(rangeText)) !== null) {
|
|
4487
|
+
const n = parseInt(rm2[1] || rm2[2] || "", 10);
|
|
4488
|
+
if (!Number.isFinite(n)) continue;
|
|
4489
|
+
const filePath = pathNear(rangeText, rm2.index);
|
|
4490
|
+
if (!filePath) continue;
|
|
4491
|
+
const cached = readCached(filePath);
|
|
4492
|
+
const lines = cached?.isBinary ? void 0 : cached?.lines;
|
|
4493
|
+
if (lines !== void 0 && n > lines) {
|
|
4494
|
+
issues.push({
|
|
4495
|
+
severity: "warning",
|
|
4496
|
+
kind: "line_range_too_large",
|
|
4497
|
+
path: filePath,
|
|
4498
|
+
detail: `Range ends at line ${n} but file has ${lines} lines`,
|
|
4499
|
+
excerpt: rm2[0].slice(0, 80)
|
|
4500
|
+
});
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
PATH_COLON_LINE_RE.lastIndex = 0;
|
|
4504
|
+
while ((rm2 = PATH_COLON_LINE_RE.exec(rangeText)) !== null) {
|
|
4505
|
+
const fp = rm2[1].trim().replace(/^\.\//, "");
|
|
4506
|
+
const n = parseInt(rm2[2], 10);
|
|
4507
|
+
if (!Number.isFinite(n)) continue;
|
|
4508
|
+
const cached = readCached(fp);
|
|
4509
|
+
const lines = cached?.isBinary ? void 0 : cached?.lines;
|
|
4510
|
+
if (lines !== void 0 && n > lines) {
|
|
4511
|
+
issues.push({
|
|
4512
|
+
severity: "warning",
|
|
4513
|
+
kind: "line_range_too_large",
|
|
4514
|
+
path: fp,
|
|
4515
|
+
detail: `Range ends at line ${n} but file has ${lines} lines`,
|
|
4516
|
+
excerpt: rm2[0].slice(0, 80)
|
|
4517
|
+
});
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
const textByLine = input5.planText.split(/\r?\n/);
|
|
4521
|
+
for (let li = 0; li < textByLine.length; li++) {
|
|
4522
|
+
const line2 = textByLine[li] ?? "";
|
|
4523
|
+
const near = [textByLine[li - 1] ?? "", line2, textByLine[li + 1] ?? ""].join("\n");
|
|
4524
|
+
const pathNearCell = near.match(/`([^`\n/]+\/[^`\n]+?\.[a-z0-9]{1,6})`/i);
|
|
4525
|
+
if (!pathNearCell) continue;
|
|
4526
|
+
const fp = pathNearCell[1].replace(/^\.\//, "");
|
|
4527
|
+
const cached = readCached(fp);
|
|
4528
|
+
if (!cached || cached.isBinary) continue;
|
|
4529
|
+
const symRe = /`([A-Za-z_][A-Za-z0-9_]{2,50})`/g;
|
|
4530
|
+
let sm;
|
|
4531
|
+
while ((sm = symRe.exec(line2)) !== null) {
|
|
4532
|
+
const sym = sm[1];
|
|
4533
|
+
if (["true", "false", "null", "undefined", "string", "number", "boolean", "void", "never"].includes(
|
|
4534
|
+
sym
|
|
4535
|
+
)) {
|
|
4536
|
+
continue;
|
|
4537
|
+
}
|
|
4538
|
+
if (!cached.text.includes(sym)) {
|
|
4539
|
+
const suggestions = suggestSymbols(cached.text, sym);
|
|
4540
|
+
issues.push({
|
|
4541
|
+
severity: "warning",
|
|
4542
|
+
kind: "symbol_not_found",
|
|
4543
|
+
path: fp,
|
|
4544
|
+
detail: `Symbol \`${sym}\` not found as substring in file` + (suggestions.length ? ` \u2014 did you mean: ${suggestions.map((s) => `\`${s}\``).join(", ")}?` : ""),
|
|
4545
|
+
excerpt: sm[0]
|
|
4546
|
+
});
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
return issues;
|
|
4551
|
+
}
|
|
4552
|
+
|
|
3876
4553
|
// src/planner/loop.ts
|
|
3877
4554
|
var MAX_RATE_LIMIT_RETRY_SEC = 90;
|
|
3878
4555
|
var defaultSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
@@ -3910,8 +4587,147 @@ function buildPlannerRunStats(budget, turns, runStartedAt) {
|
|
|
3910
4587
|
durationMs: Date.now() - runStartedAt
|
|
3911
4588
|
};
|
|
3912
4589
|
}
|
|
3913
|
-
function
|
|
3914
|
-
|
|
4590
|
+
function buildScoutedContextSection(root, budget, selectedFiles, bus, runId, readRanges) {
|
|
4591
|
+
const normalizeRel = (p) => p.replace(/^\.\//, "");
|
|
4592
|
+
const rangeByPath = /* @__PURE__ */ new Map();
|
|
4593
|
+
for (const rr of readRanges ?? []) {
|
|
4594
|
+
rangeByPath.set(normalizeRel(rr.path), rr);
|
|
4595
|
+
}
|
|
4596
|
+
const ordered = [];
|
|
4597
|
+
const seenPath = /* @__PURE__ */ new Set();
|
|
4598
|
+
for (const f of selectedFiles) {
|
|
4599
|
+
const c = normalizeRel(f);
|
|
4600
|
+
if (!seenPath.has(c)) {
|
|
4601
|
+
seenPath.add(c);
|
|
4602
|
+
ordered.push(c);
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
for (const p of rangeByPath.keys()) {
|
|
4606
|
+
if (!seenPath.has(p)) {
|
|
4607
|
+
seenPath.add(p);
|
|
4608
|
+
ordered.push(p);
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
const blocks = ["\n## Scouted context (already loaded)\n"];
|
|
4612
|
+
for (const cleaned of ordered) {
|
|
4613
|
+
const rr = rangeByPath.get(cleaned);
|
|
4614
|
+
if (rr) {
|
|
4615
|
+
const result = readFileTool(root, budget, { path: cleaned, offset: rr.offset, limit: rr.limit });
|
|
4616
|
+
const hi = rr.offset + rr.limit - 1;
|
|
4617
|
+
if (result.isError) {
|
|
4618
|
+
blocks.push(
|
|
4619
|
+
`
|
|
4620
|
+
### \`${cleaned}\` (lines ${rr.offset}\u2013${hi})
|
|
4621
|
+
|
|
4622
|
+
_${result.content}_
|
|
4623
|
+
`
|
|
4624
|
+
);
|
|
4625
|
+
} else {
|
|
4626
|
+
blocks.push(
|
|
4627
|
+
`
|
|
4628
|
+
### \`${cleaned}\` (lines ${rr.offset}\u2013${hi})
|
|
4629
|
+
|
|
4630
|
+
\`\`\`
|
|
4631
|
+
${result.content}
|
|
4632
|
+
\`\`\`
|
|
4633
|
+
`
|
|
4634
|
+
);
|
|
4635
|
+
}
|
|
4636
|
+
continue;
|
|
4637
|
+
}
|
|
4638
|
+
const resolved = path19.resolve(root, cleaned);
|
|
4639
|
+
const rc = path19.relative(root, resolved);
|
|
4640
|
+
if (rc.startsWith("..") || path19.isAbsolute(rc)) {
|
|
4641
|
+
bus.emit({
|
|
4642
|
+
kind: "validation_issue",
|
|
4643
|
+
runId,
|
|
4644
|
+
severity: "warning",
|
|
4645
|
+
issueKind: "missing_path",
|
|
4646
|
+
path: cleaned,
|
|
4647
|
+
detail: "Scout-selected path escapes the project root"
|
|
4648
|
+
});
|
|
4649
|
+
continue;
|
|
4650
|
+
}
|
|
4651
|
+
if (!fs27.existsSync(resolved)) {
|
|
4652
|
+
bus.emit({
|
|
4653
|
+
kind: "validation_issue",
|
|
4654
|
+
runId,
|
|
4655
|
+
severity: "warning",
|
|
4656
|
+
issueKind: "missing_path",
|
|
4657
|
+
path: cleaned,
|
|
4658
|
+
detail: "Scout selected this path but it does not exist on disk"
|
|
4659
|
+
});
|
|
4660
|
+
continue;
|
|
4661
|
+
}
|
|
4662
|
+
const st = fs27.statSync(resolved);
|
|
4663
|
+
if (!st.isFile()) continue;
|
|
4664
|
+
const n = Math.min(st.size, MAX_BYTES_PER_FULL_READ);
|
|
4665
|
+
const cap = budget.canRead(n);
|
|
4666
|
+
if (!cap.ok) {
|
|
4667
|
+
blocks.push(`
|
|
4668
|
+
### \`${cleaned}\`
|
|
4669
|
+
|
|
4670
|
+
_(skipped: ${cap.reason})_
|
|
4671
|
+
`);
|
|
4672
|
+
continue;
|
|
4673
|
+
}
|
|
4674
|
+
let buf;
|
|
4675
|
+
try {
|
|
4676
|
+
buf = fs27.readFileSync(resolved);
|
|
4677
|
+
} catch {
|
|
4678
|
+
continue;
|
|
4679
|
+
}
|
|
4680
|
+
if (buf.length > MAX_BYTES_PER_FULL_READ) {
|
|
4681
|
+
const head = buf.subarray(0, MAX_BYTES_PER_FULL_READ);
|
|
4682
|
+
if (looksBinary(head)) {
|
|
4683
|
+
blocks.push(`
|
|
4684
|
+
### \`${cleaned}\`
|
|
4685
|
+
|
|
4686
|
+
_(binary file skipped)_
|
|
4687
|
+
`);
|
|
4688
|
+
continue;
|
|
4689
|
+
}
|
|
4690
|
+
budget.recordRead(head.length);
|
|
4691
|
+
blocks.push(
|
|
4692
|
+
`
|
|
4693
|
+
### \`${cleaned}\` (truncated: first ${MAX_BYTES_PER_FULL_READ}-byte head; full size ${buf.length} bytes)
|
|
4694
|
+
|
|
4695
|
+
\`\`\`
|
|
4696
|
+
${head.toString("utf8")}
|
|
4697
|
+
\`\`\`
|
|
4698
|
+
`
|
|
4699
|
+
);
|
|
4700
|
+
continue;
|
|
4701
|
+
}
|
|
4702
|
+
if (looksBinary(buf)) {
|
|
4703
|
+
blocks.push(`
|
|
4704
|
+
### \`${cleaned}\`
|
|
4705
|
+
|
|
4706
|
+
_(binary file skipped)_
|
|
4707
|
+
`);
|
|
4708
|
+
continue;
|
|
4709
|
+
}
|
|
4710
|
+
budget.recordRead(buf.length);
|
|
4711
|
+
blocks.push(`
|
|
4712
|
+
### \`${cleaned}\`
|
|
4713
|
+
|
|
4714
|
+
\`\`\`
|
|
4715
|
+
${buf.toString("utf8")}
|
|
4716
|
+
\`\`\`
|
|
4717
|
+
`);
|
|
4718
|
+
}
|
|
4719
|
+
return blocks.join("");
|
|
4720
|
+
}
|
|
4721
|
+
function scoutStageStats(usage, durationMs) {
|
|
4722
|
+
return {
|
|
4723
|
+
turns: 0,
|
|
4724
|
+
inputTokens: usage.inputTokens,
|
|
4725
|
+
outputTokens: usage.outputTokens,
|
|
4726
|
+
cacheCreationTokens: 0,
|
|
4727
|
+
cacheReadTokens: 0,
|
|
4728
|
+
cacheHitRatio: 0,
|
|
4729
|
+
durationMs
|
|
4730
|
+
};
|
|
3915
4731
|
}
|
|
3916
4732
|
function extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut) {
|
|
3917
4733
|
input5.budget.extendSession();
|
|
@@ -3951,28 +4767,106 @@ function cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, i
|
|
|
3951
4767
|
userCancelled: true
|
|
3952
4768
|
};
|
|
3953
4769
|
}
|
|
3954
|
-
|
|
4770
|
+
function asToolInput(args) {
|
|
4771
|
+
if (args && typeof args === "object" && !Array.isArray(args)) return args;
|
|
4772
|
+
if (typeof args === "string") {
|
|
4773
|
+
try {
|
|
4774
|
+
return JSON.parse(args);
|
|
4775
|
+
} catch {
|
|
4776
|
+
return {};
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4779
|
+
return {};
|
|
4780
|
+
}
|
|
4781
|
+
function normalizeToolExecutionResult(result) {
|
|
4782
|
+
if (typeof result === "string") {
|
|
4783
|
+
try {
|
|
4784
|
+
const j = JSON.parse(result);
|
|
4785
|
+
if (j && typeof j === "object" && !Array.isArray(j) && "content" in j) {
|
|
4786
|
+
const o = j;
|
|
4787
|
+
return {
|
|
4788
|
+
content: typeof o.content === "string" ? o.content : JSON.stringify(o.content ?? ""),
|
|
4789
|
+
isError: o.isError === true
|
|
4790
|
+
};
|
|
4791
|
+
}
|
|
4792
|
+
} catch {
|
|
4793
|
+
}
|
|
4794
|
+
return { content: result, isError: false };
|
|
4795
|
+
}
|
|
4796
|
+
if (result && typeof result === "object" && !Array.isArray(result) && "content" in result) {
|
|
4797
|
+
const o = result;
|
|
4798
|
+
return {
|
|
4799
|
+
content: typeof o.content === "string" ? o.content : JSON.stringify(o.content ?? ""),
|
|
4800
|
+
isError: o.isError === true
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
return { content: typeof result === "undefined" ? "" : JSON.stringify(result), isError: false };
|
|
4804
|
+
}
|
|
4805
|
+
function applyStepToTurns(turns, step2) {
|
|
4806
|
+
const callsById = /* @__PURE__ */ new Map();
|
|
4807
|
+
const toolCalls = step2.toolCalls.map((tc) => {
|
|
4808
|
+
callsById.set(tc.toolCallId, tc.toolName);
|
|
4809
|
+
return { id: tc.toolCallId, name: tc.toolName, input: asToolInput(tc.args) };
|
|
4810
|
+
});
|
|
4811
|
+
turns.push({
|
|
4812
|
+
role: "assistant",
|
|
4813
|
+
...step2.text ? { text: step2.text } : {},
|
|
4814
|
+
...toolCalls.length ? { toolCalls } : {}
|
|
4815
|
+
});
|
|
4816
|
+
if (step2.toolResults.length) {
|
|
4817
|
+
const toolResults = step2.toolResults.map((tr) => {
|
|
4818
|
+
const norm = normalizeToolExecutionResult(tr.result);
|
|
4819
|
+
return {
|
|
4820
|
+
toolCallId: tr.toolCallId,
|
|
4821
|
+
name: callsById.get(tr.toolCallId) ?? "read_file",
|
|
4822
|
+
content: norm.content,
|
|
4823
|
+
...norm.isError ? { isError: true } : {}
|
|
4824
|
+
};
|
|
4825
|
+
});
|
|
4826
|
+
turns.push({ role: "user", toolResults });
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
function providerResponseFromApiError(provider, modelId, err2) {
|
|
4830
|
+
const body = err2.responseBody ?? "";
|
|
4831
|
+
const nf = detectModelNotFound(provider, modelId, err2.statusCode ?? 0, body);
|
|
4832
|
+
if (nf) {
|
|
4833
|
+
return {
|
|
4834
|
+
stopReason: "error",
|
|
4835
|
+
errorKind: "model_not_found",
|
|
4836
|
+
rawError: modelNotFoundMessage(nf)
|
|
4837
|
+
};
|
|
4838
|
+
}
|
|
4839
|
+
const rl = detectRateLimit(provider, err2.statusCode ?? 0, err2.responseHeaders ?? {}, body);
|
|
4840
|
+
if (rl) {
|
|
4841
|
+
return {
|
|
4842
|
+
stopReason: "error",
|
|
4843
|
+
errorKind: "rate_limit",
|
|
4844
|
+
retryAfterSec: rl.retryAfterSec,
|
|
4845
|
+
rawError: `${provider} ${err2.statusCode ?? "error"}: ${body.slice(0, 500)}`
|
|
4846
|
+
};
|
|
4847
|
+
}
|
|
4848
|
+
return {
|
|
4849
|
+
stopReason: "error",
|
|
4850
|
+
rawError: `${provider} ${err2.statusCode ?? "error"}: ${body.slice(0, 500)}`
|
|
4851
|
+
};
|
|
4852
|
+
}
|
|
4853
|
+
async function runDraftStage(input5) {
|
|
3955
4854
|
const runStartedAt = Date.now();
|
|
3956
4855
|
const bus = input5.events ?? new PlannerEventBus();
|
|
3957
4856
|
const runId = input5.runId ?? newRunId();
|
|
3958
4857
|
const cacheEnabled = input5.cacheEnabled ?? true;
|
|
3959
|
-
bus.emit({
|
|
3960
|
-
kind: "started",
|
|
3961
|
-
runId,
|
|
3962
|
-
provider: input5.provider.name,
|
|
3963
|
-
model: input5.model,
|
|
3964
|
-
cacheEnabled
|
|
3965
|
-
});
|
|
3966
4858
|
const baseMaxIter = input5.maxIterations ?? DEFAULT_PLANNER_MAX_ITERATIONS;
|
|
3967
4859
|
const maxIter = { current: baseMaxIter };
|
|
3968
4860
|
const baseMaxOut = input5.maxOutputTokens ?? DEFAULT_PLANNER_MAX_OUTPUT_TOKENS;
|
|
3969
4861
|
const maxOut = { current: baseMaxOut };
|
|
3970
4862
|
const turns = [{ role: "user", text: input5.userPrompt }];
|
|
4863
|
+
if (input5.scoutedSection?.trim()) {
|
|
4864
|
+
turns.push({ role: "user", text: composeScoutedUserMessage({ scoutedSection: input5.scoutedSection }) });
|
|
4865
|
+
}
|
|
3971
4866
|
let accumulatedText = "";
|
|
3972
4867
|
let iterations = 0;
|
|
3973
4868
|
let budgetExhausted = false;
|
|
3974
4869
|
let finishedNormally = false;
|
|
3975
|
-
let prevRequestPrefix;
|
|
3976
4870
|
const decide = input5.decideOnLimit;
|
|
3977
4871
|
const sleepFn = input5.sleep ?? defaultSleep;
|
|
3978
4872
|
const limitCtx = (kind) => ({
|
|
@@ -4043,126 +4937,160 @@ async function runPlanner(input5) {
|
|
|
4043
4937
|
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
4044
4938
|
};
|
|
4045
4939
|
}
|
|
4046
|
-
if (input5.budget.overCost()) {
|
|
4047
|
-
if (decide) {
|
|
4048
|
-
const d = await decide(limitCtx("cost_cap"));
|
|
4049
|
-
if (d === "cancel") {
|
|
4050
|
-
return {
|
|
4051
|
-
planText: accumulatedText,
|
|
4052
|
-
budgetExhausted,
|
|
4053
|
-
timedOut: false,
|
|
4054
|
-
finishedNormally: false,
|
|
4055
|
-
iterations,
|
|
4056
|
-
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
|
|
4057
|
-
incompleteKind: "cost_cap",
|
|
4058
|
-
userCancelled: true
|
|
4059
|
-
};
|
|
4060
|
-
}
|
|
4061
|
-
extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut);
|
|
4062
|
-
iterations -= 1;
|
|
4063
|
-
continue;
|
|
4064
|
-
}
|
|
4065
|
-
return {
|
|
4066
|
-
planText: accumulatedText,
|
|
4067
|
-
budgetExhausted: true,
|
|
4068
|
-
timedOut: false,
|
|
4069
|
-
finishedNormally: false,
|
|
4070
|
-
iterations,
|
|
4071
|
-
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
4072
|
-
};
|
|
4073
|
-
}
|
|
4074
4940
|
if (iterations >= 2) {
|
|
4075
4941
|
bus.emit({ kind: "turn_started", runId, turn: iterations });
|
|
4076
4942
|
}
|
|
4077
|
-
const
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4943
|
+
const tools = buildPlannerTools({
|
|
4944
|
+
root: input5.root,
|
|
4945
|
+
budget: input5.budget,
|
|
4946
|
+
enabled: {
|
|
4947
|
+
grep: input5.toolsEnabled?.grep ?? true,
|
|
4948
|
+
listDir: input5.toolsEnabled?.listDir ?? true,
|
|
4949
|
+
rangedRead: input5.toolsEnabled?.rangedRead ?? true
|
|
4950
|
+
},
|
|
4951
|
+
readHooks: {
|
|
4952
|
+
runId,
|
|
4953
|
+
turn: iterations,
|
|
4954
|
+
bus,
|
|
4955
|
+
onToolCall: input5.onToolCall,
|
|
4956
|
+
decideOnLimit: input5.decideOnLimit,
|
|
4957
|
+
getLimitCtx: () => limitCtx("file_or_context_reads"),
|
|
4958
|
+
extendSessionLimits: () => extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut),
|
|
4959
|
+
getAccumulatedText: () => accumulatedText,
|
|
4960
|
+
setBudgetExhausted: (v) => {
|
|
4961
|
+
budgetExhausted = v;
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
});
|
|
4965
|
+
const messages = turnsToCoreMessages(input5.systemPrompt, turns, {
|
|
4084
4966
|
cacheEnabled,
|
|
4085
|
-
|
|
4086
|
-
};
|
|
4967
|
+
provider: input5.provider
|
|
4968
|
+
});
|
|
4087
4969
|
bus.emit({ kind: "request_sent", runId, turn: iterations });
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
if (!currentPrefix.startsWith(prevRequestPrefix)) {
|
|
4091
|
-
const byte = firstByteDiff(prevRequestPrefix, currentPrefix);
|
|
4092
|
-
warning(
|
|
4093
|
-
`planner: prefix mutation at turn ${iterations}, byte ${byte} \u2014 caching will not hit.`
|
|
4094
|
-
);
|
|
4095
|
-
}
|
|
4096
|
-
}
|
|
4097
|
-
let response;
|
|
4098
|
-
try {
|
|
4099
|
-
response = await input5.provider.send(request);
|
|
4100
|
-
} catch (e) {
|
|
4101
|
-
if (isAbortError(e)) {
|
|
4102
|
-
return cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, iterations, runStartedAt);
|
|
4103
|
-
}
|
|
4104
|
-
throw e;
|
|
4105
|
-
}
|
|
4106
|
-
if (response.usage) {
|
|
4107
|
-
recordUsageAndEmit(bus, input5, runId, iterations, response.usage);
|
|
4108
|
-
}
|
|
4109
|
-
let retriedRateLimit = false;
|
|
4970
|
+
let attempt = 0;
|
|
4971
|
+
let stepResult;
|
|
4110
4972
|
let retrySkippedReason;
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
const
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
retrySkippedReason = "retry_after_too_long";
|
|
4129
|
-
} else {
|
|
4130
|
-
const waitSec = Math.min(asked ?? 10, MAX_RATE_LIMIT_RETRY_SEC);
|
|
4131
|
-
emitRateLimit("retrying", waitSec);
|
|
4132
|
-
input5.onRateLimit?.(waitSec);
|
|
4133
|
-
try {
|
|
4134
|
-
await sleepWithAbort(waitSec * 1e3, sleepFn, input5.abort);
|
|
4135
|
-
} catch (e) {
|
|
4136
|
-
if (isAbortError(e)) {
|
|
4137
|
-
return cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, iterations, runStartedAt);
|
|
4973
|
+
let retriedRateLimit = false;
|
|
4974
|
+
const callStream = async () => {
|
|
4975
|
+
const { streamText } = await import("ai");
|
|
4976
|
+
const stream = streamText({
|
|
4977
|
+
model: input5.model,
|
|
4978
|
+
messages,
|
|
4979
|
+
tools,
|
|
4980
|
+
toolChoice: "auto",
|
|
4981
|
+
maxSteps: 1,
|
|
4982
|
+
maxRetries: 0,
|
|
4983
|
+
maxTokens: maxOut.current,
|
|
4984
|
+
abortSignal: input5.abort,
|
|
4985
|
+
onChunk: ({ chunk }) => {
|
|
4986
|
+
if (chunk.type === "text-delta") {
|
|
4987
|
+
accumulatedText += chunk.textDelta;
|
|
4988
|
+
input5.onAssistantText?.(chunk.textDelta);
|
|
4989
|
+
bus.emit({ kind: "assistant_text", runId, turn: iterations, delta: chunk.textDelta });
|
|
4138
4990
|
}
|
|
4139
|
-
|
|
4991
|
+
},
|
|
4992
|
+
onStepFinish: (step2) => {
|
|
4993
|
+
const u = usageFromLanguageModelStep(step2.usage, step2.providerMetadata);
|
|
4994
|
+
recordUsageAndEmit(bus, input5, runId, iterations, u);
|
|
4995
|
+
}
|
|
4996
|
+
});
|
|
4997
|
+
for await (const part of stream.fullStream) {
|
|
4998
|
+
const p = part;
|
|
4999
|
+
if (p.type === "error") {
|
|
5000
|
+
throw p.error;
|
|
4140
5001
|
}
|
|
5002
|
+
}
|
|
5003
|
+
const steps = await stream.steps;
|
|
5004
|
+
const last = steps[steps.length - 1];
|
|
5005
|
+
if (!last) throw new Error("planner: empty model step");
|
|
5006
|
+
stepResult = {
|
|
5007
|
+
text: last.text,
|
|
5008
|
+
finishReason: last.finishReason,
|
|
5009
|
+
toolCalls: last.toolCalls.flatMap(
|
|
5010
|
+
(tc) => tc ? [
|
|
5011
|
+
{
|
|
5012
|
+
toolCallId: tc.toolCallId,
|
|
5013
|
+
toolName: tc.toolName,
|
|
5014
|
+
args: tc.args
|
|
5015
|
+
}
|
|
5016
|
+
] : []
|
|
5017
|
+
),
|
|
5018
|
+
toolResults: last.toolResults.flatMap(
|
|
5019
|
+
(tr) => tr ? [{ toolCallId: tr.toolCallId, result: tr.result }] : []
|
|
5020
|
+
),
|
|
5021
|
+
usage: last.usage,
|
|
5022
|
+
providerMetadata: last.providerMetadata
|
|
5023
|
+
};
|
|
5024
|
+
};
|
|
5025
|
+
try {
|
|
5026
|
+
while (true) {
|
|
4141
5027
|
try {
|
|
4142
|
-
|
|
5028
|
+
await callStream();
|
|
5029
|
+
break;
|
|
4143
5030
|
} catch (e) {
|
|
5031
|
+
if (PlannerUserCancelledError.is(e)) {
|
|
5032
|
+
return {
|
|
5033
|
+
planText: e.planText,
|
|
5034
|
+
budgetExhausted: false,
|
|
5035
|
+
timedOut: false,
|
|
5036
|
+
finishedNormally: false,
|
|
5037
|
+
iterations,
|
|
5038
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
|
|
5039
|
+
incompleteKind: "budget_reads",
|
|
5040
|
+
userCancelled: true
|
|
5041
|
+
};
|
|
5042
|
+
}
|
|
4144
5043
|
if (isAbortError(e)) {
|
|
4145
5044
|
return cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, iterations, runStartedAt);
|
|
4146
5045
|
}
|
|
5046
|
+
if (APICallError2.isInstance(e)) {
|
|
5047
|
+
const synthetic = providerResponseFromApiError(input5.provider, input5.modelId, e);
|
|
5048
|
+
if (synthetic.errorKind === "rate_limit") {
|
|
5049
|
+
const asked = synthetic.retryAfterSec;
|
|
5050
|
+
const emitRateLimit = (phase, waitSec) => {
|
|
5051
|
+
bus.emit({
|
|
5052
|
+
kind: "rate_limit",
|
|
5053
|
+
runId,
|
|
5054
|
+
turn: iterations,
|
|
5055
|
+
retryAfterSec: synthetic.retryAfterSec,
|
|
5056
|
+
waitSec,
|
|
5057
|
+
capSec: MAX_RATE_LIMIT_RETRY_SEC,
|
|
5058
|
+
phase,
|
|
5059
|
+
provider: input5.provider,
|
|
5060
|
+
rawBody: (synthetic.rawError ?? "").slice(0, 200)
|
|
5061
|
+
});
|
|
5062
|
+
};
|
|
5063
|
+
if (asked !== void 0 && asked > MAX_RATE_LIMIT_RETRY_SEC) {
|
|
5064
|
+
emitRateLimit("aborted", asked);
|
|
5065
|
+
retrySkippedReason = "retry_after_too_long";
|
|
5066
|
+
throw composePlannerError(input5.provider, synthetic, false, retrySkippedReason);
|
|
5067
|
+
}
|
|
5068
|
+
if (attempt === 0) {
|
|
5069
|
+
const waitSec = Math.min(asked ?? 10, MAX_RATE_LIMIT_RETRY_SEC);
|
|
5070
|
+
emitRateLimit("retrying", waitSec);
|
|
5071
|
+
input5.onRateLimit?.(waitSec);
|
|
5072
|
+
await sleepWithAbort(waitSec * 1e3, sleepFn, input5.abort);
|
|
5073
|
+
attempt += 1;
|
|
5074
|
+
retriedRateLimit = true;
|
|
5075
|
+
continue;
|
|
5076
|
+
}
|
|
5077
|
+
throw composePlannerError(input5.provider, synthetic, true, retrySkippedReason);
|
|
5078
|
+
}
|
|
5079
|
+
throw composePlannerError(input5.provider, synthetic, retriedRateLimit, retrySkippedReason);
|
|
5080
|
+
}
|
|
4147
5081
|
throw e;
|
|
4148
5082
|
}
|
|
4149
|
-
if (response.usage) {
|
|
4150
|
-
recordUsageAndEmit(bus, input5, runId, iterations, response.usage);
|
|
4151
|
-
}
|
|
4152
|
-
retriedRateLimit = true;
|
|
4153
5083
|
}
|
|
5084
|
+
} catch (e) {
|
|
5085
|
+
if (isAbortError(e)) {
|
|
5086
|
+
return cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, iterations, runStartedAt);
|
|
5087
|
+
}
|
|
5088
|
+
throw e;
|
|
4154
5089
|
}
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
if (response.text) {
|
|
4160
|
-
accumulatedText += response.text;
|
|
4161
|
-
input5.onAssistantText?.(response.text);
|
|
4162
|
-
bus.emit({ kind: "assistant_text", runId, turn: iterations, delta: response.text });
|
|
4163
|
-
}
|
|
4164
|
-
if (response.stopReason === "max_tokens" && !response.toolCalls?.length) {
|
|
4165
|
-
bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: response.stopReason });
|
|
5090
|
+
const finishReason = stepResult.finishReason;
|
|
5091
|
+
const toolCallsLen = stepResult.toolCalls.length;
|
|
5092
|
+
if (finishReason === "length" && toolCallsLen === 0) {
|
|
5093
|
+
bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: "max_tokens" });
|
|
4166
5094
|
if (decide) {
|
|
4167
5095
|
const d = await decide(limitCtx("max_output_tokens"));
|
|
4168
5096
|
if (d === "cancel") {
|
|
@@ -4178,7 +5106,7 @@ async function runPlanner(input5) {
|
|
|
4178
5106
|
};
|
|
4179
5107
|
}
|
|
4180
5108
|
extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut);
|
|
4181
|
-
turns
|
|
5109
|
+
applyStepToTurns(turns, stepResult);
|
|
4182
5110
|
turns.push({ role: "user", text: PLANNER_MARKDOWN_CONTINUATION_USER });
|
|
4183
5111
|
iterations -= 1;
|
|
4184
5112
|
continue;
|
|
@@ -4193,8 +5121,13 @@ async function runPlanner(input5) {
|
|
|
4193
5121
|
incompleteKind: "max_output_tokens"
|
|
4194
5122
|
};
|
|
4195
5123
|
}
|
|
4196
|
-
if (
|
|
4197
|
-
|
|
5124
|
+
if (finishReason === "error" || finishReason === "content-filter") {
|
|
5125
|
+
throw new Error(
|
|
5126
|
+
stepResult.text || `planner: model reported ${finishReason}. Run \`squad doctor\` to diagnose, or retry \u2014 most 5xx errors are transient.`
|
|
5127
|
+
);
|
|
5128
|
+
}
|
|
5129
|
+
if ((finishReason === "stop" || finishReason === "unknown") && toolCallsLen === 0) {
|
|
5130
|
+
bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
|
|
4198
5131
|
finishedNormally = true;
|
|
4199
5132
|
return {
|
|
4200
5133
|
planText: accumulatedText,
|
|
@@ -4205,81 +5138,173 @@ async function runPlanner(input5) {
|
|
|
4205
5138
|
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
4206
5139
|
};
|
|
4207
5140
|
}
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
input5.onToolCall?.(tc, bytesLoaded, after);
|
|
4226
|
-
bus.emit({
|
|
4227
|
-
kind: "tool_call",
|
|
4228
|
-
runId,
|
|
4229
|
-
turn: iterations,
|
|
4230
|
-
toolCall: tc,
|
|
4231
|
-
bytesLoaded,
|
|
4232
|
-
totalBytes: after
|
|
5141
|
+
if (toolCallsLen === 0) {
|
|
5142
|
+
bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
|
|
5143
|
+
finishedNormally = true;
|
|
5144
|
+
return {
|
|
5145
|
+
planText: accumulatedText,
|
|
5146
|
+
budgetExhausted,
|
|
5147
|
+
timedOut: false,
|
|
5148
|
+
finishedNormally,
|
|
5149
|
+
iterations,
|
|
5150
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
5151
|
+
};
|
|
5152
|
+
}
|
|
5153
|
+
applyStepToTurns(turns, stepResult);
|
|
5154
|
+
if (budgetExhausted) {
|
|
5155
|
+
turns.push({
|
|
5156
|
+
role: "user",
|
|
5157
|
+
text: "Budget is exhausted. Finalise the plan with the information you already have. Do not call any more tools. Output the complete plan markdown now."
|
|
4233
5158
|
});
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
5159
|
+
}
|
|
5160
|
+
bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
|
|
5161
|
+
}
|
|
5162
|
+
}
|
|
5163
|
+
async function runPlanner(input5) {
|
|
5164
|
+
const bus = input5.events ?? new PlannerEventBus();
|
|
5165
|
+
const runId = input5.runId ?? newRunId();
|
|
5166
|
+
const cacheEnabled = input5.cacheEnabled ?? true;
|
|
5167
|
+
bus.emit({
|
|
5168
|
+
kind: "started",
|
|
5169
|
+
runId,
|
|
5170
|
+
provider: input5.provider,
|
|
5171
|
+
model: input5.modelId,
|
|
5172
|
+
cacheEnabled
|
|
5173
|
+
});
|
|
5174
|
+
const merged = { ...input5, events: bus, runId };
|
|
5175
|
+
const systemPrompt = input5.systemPrompt;
|
|
5176
|
+
let scoutSummary;
|
|
5177
|
+
let scoutStats;
|
|
5178
|
+
let scoutedSection;
|
|
5179
|
+
const scoutOn = input5.stages?.scout?.enabled !== false;
|
|
5180
|
+
const scoutModel = input5.stages?.scout?.model;
|
|
5181
|
+
const maxScoutFiles = input5.stages?.scout?.maxFiles ?? 12;
|
|
5182
|
+
const scoutMaxOut = input5.stages?.scout?.maxOutputTokens ?? 2048;
|
|
5183
|
+
const scoutModelIdStr = input5.stages?.scout?.modelId ?? "";
|
|
5184
|
+
if (scoutOn && scoutModel) {
|
|
5185
|
+
if (!input5.scoutSystemPrompt?.trim()) {
|
|
5186
|
+
throw new Error(
|
|
5187
|
+
"planner: scout enabled but `scoutSystemPrompt` was not provided. Compose it via composeScoutSystemPrompt() or set stages.scout.enabled = false."
|
|
5188
|
+
);
|
|
5189
|
+
}
|
|
5190
|
+
const t0 = Date.now();
|
|
5191
|
+
bus.emit({ kind: "stage_started", runId, stage: "scout" });
|
|
5192
|
+
try {
|
|
5193
|
+
const scoutRes = await runScout({
|
|
5194
|
+
model: scoutModel,
|
|
5195
|
+
systemPrompt: input5.scoutSystemPrompt,
|
|
5196
|
+
userPrompt: input5.userPrompt,
|
|
5197
|
+
budget: input5.budget,
|
|
5198
|
+
abort: input5.abort,
|
|
5199
|
+
maxTokens: scoutMaxOut
|
|
5200
|
+
});
|
|
5201
|
+
const durationMs = Date.now() - t0;
|
|
5202
|
+
if (scoutRes) {
|
|
5203
|
+
const files = scoutRes.output.selectedFiles.slice(0, maxScoutFiles);
|
|
5204
|
+
const tokensUsed = scoutRes.usage.inputTokens + scoutRes.usage.outputTokens;
|
|
5205
|
+
scoutSummary = {
|
|
5206
|
+
selected: files,
|
|
5207
|
+
reasoning: scoutRes.output.reasoning,
|
|
5208
|
+
durationMs,
|
|
5209
|
+
tokensUsed
|
|
5210
|
+
};
|
|
5211
|
+
scoutStats = scoutStageStats(scoutRes.usage, durationMs);
|
|
5212
|
+
bus.emit({
|
|
5213
|
+
kind: "scout_result",
|
|
5214
|
+
runId,
|
|
5215
|
+
selected: files,
|
|
5216
|
+
reasoning: scoutRes.output.reasoning
|
|
5217
|
+
});
|
|
5218
|
+
scoutedSection = buildScoutedContextSection(
|
|
5219
|
+
input5.root,
|
|
5220
|
+
input5.budget,
|
|
5221
|
+
files,
|
|
5222
|
+
bus,
|
|
5223
|
+
runId,
|
|
5224
|
+
scoutRes.output.readRanges
|
|
5225
|
+
);
|
|
5226
|
+
bus.emit({
|
|
5227
|
+
kind: "stage_complete",
|
|
5228
|
+
runId,
|
|
5229
|
+
stage: "scout",
|
|
5230
|
+
success: true,
|
|
5231
|
+
durationMs,
|
|
5232
|
+
tokensUsed
|
|
5233
|
+
});
|
|
5234
|
+
} else {
|
|
5235
|
+
bus.emit({
|
|
5236
|
+
kind: "stage_complete",
|
|
5237
|
+
runId,
|
|
5238
|
+
stage: "scout",
|
|
5239
|
+
success: false,
|
|
5240
|
+
durationMs
|
|
5241
|
+
});
|
|
5242
|
+
}
|
|
5243
|
+
} catch (err2) {
|
|
5244
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
5245
|
+
if (APICallError2.isInstance(err2)) {
|
|
5246
|
+
const status = err2.statusCode ?? 0;
|
|
5247
|
+
if (status === 401 || status === 403) {
|
|
5248
|
+
throw err2;
|
|
5249
|
+
}
|
|
5250
|
+
if (detectModelNotFound(input5.provider, scoutModelIdStr, status, err2.responseBody ?? "")) {
|
|
5251
|
+
throw err2;
|
|
4263
5252
|
}
|
|
4264
5253
|
}
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
5254
|
+
bus.emit({
|
|
5255
|
+
kind: "stage_complete",
|
|
5256
|
+
runId,
|
|
5257
|
+
stage: "scout",
|
|
5258
|
+
success: false,
|
|
5259
|
+
durationMs: Date.now() - t0,
|
|
5260
|
+
errorMessage: msg.slice(0, 200)
|
|
4272
5261
|
});
|
|
4273
5262
|
}
|
|
4274
|
-
bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: response.stopReason });
|
|
4275
5263
|
}
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
const
|
|
4279
|
-
|
|
4280
|
-
|
|
5264
|
+
const draftStarted = Date.now();
|
|
5265
|
+
bus.emit({ kind: "stage_started", runId, stage: "draft" });
|
|
5266
|
+
const draft = await runDraftStage({ ...merged, systemPrompt, scoutedSection });
|
|
5267
|
+
bus.emit({
|
|
5268
|
+
kind: "stage_complete",
|
|
5269
|
+
runId,
|
|
5270
|
+
stage: "draft",
|
|
5271
|
+
success: draft.finishedNormally && !draft.timedOut && !draft.userCancelled,
|
|
5272
|
+
durationMs: Date.now() - draftStarted
|
|
5273
|
+
});
|
|
5274
|
+
const validationOn = input5.validation?.enabled !== false;
|
|
5275
|
+
let issues = [];
|
|
5276
|
+
let validationDurationMs = 0;
|
|
5277
|
+
if (validationOn && draft.planText.trim()) {
|
|
5278
|
+
const v0 = Date.now();
|
|
5279
|
+
bus.emit({ kind: "stage_started", runId, stage: "validation" });
|
|
5280
|
+
issues = validatePlan({ root: input5.root, planText: draft.planText });
|
|
5281
|
+
for (const iss of issues.slice(0, 100)) {
|
|
5282
|
+
bus.emit({
|
|
5283
|
+
kind: "validation_issue",
|
|
5284
|
+
runId,
|
|
5285
|
+
severity: iss.severity,
|
|
5286
|
+
issueKind: iss.kind,
|
|
5287
|
+
path: iss.path,
|
|
5288
|
+
detail: iss.detail,
|
|
5289
|
+
excerpt: iss.excerpt
|
|
5290
|
+
});
|
|
5291
|
+
}
|
|
5292
|
+
validationDurationMs = Date.now() - v0;
|
|
5293
|
+
bus.emit({
|
|
5294
|
+
kind: "stage_complete",
|
|
5295
|
+
runId,
|
|
5296
|
+
stage: "validation",
|
|
5297
|
+
success: true,
|
|
5298
|
+
durationMs: validationDurationMs
|
|
5299
|
+
});
|
|
4281
5300
|
}
|
|
4282
|
-
return
|
|
5301
|
+
return {
|
|
5302
|
+
...draft,
|
|
5303
|
+
stats: draft.stats,
|
|
5304
|
+
stagesStats: { scout: scoutStats, draft: draft.stats },
|
|
5305
|
+
scout: scoutSummary,
|
|
5306
|
+
validation: validationOn ? { issues, durationMs: validationDurationMs } : { issues: [], durationMs: 0 }
|
|
5307
|
+
};
|
|
4283
5308
|
}
|
|
4284
5309
|
function composePlannerError(providerName, response, retriedRateLimit, retrySkippedReason) {
|
|
4285
5310
|
if (response.errorKind === "rate_limit") {
|
|
@@ -4318,45 +5343,39 @@ function kindTitle(kind) {
|
|
|
4318
5343
|
return "Model round limit";
|
|
4319
5344
|
case "wall_clock":
|
|
4320
5345
|
return "Wall-clock time limit";
|
|
4321
|
-
case "cost_cap":
|
|
4322
|
-
return "Estimated cost cap";
|
|
4323
5346
|
case "file_or_context_reads":
|
|
4324
5347
|
return "File read or context size limit";
|
|
4325
5348
|
}
|
|
4326
5349
|
}
|
|
4327
|
-
function
|
|
5350
|
+
function tokenSummary(ctx) {
|
|
4328
5351
|
const snap = ctx.budgetSnapshot;
|
|
4329
|
-
|
|
5352
|
+
return `Tokens so far: ${snap.usage.inputTokens} in / ${snap.usage.outputTokens} out for this session.`;
|
|
5353
|
+
}
|
|
5354
|
+
function kindDetail(ctx) {
|
|
4330
5355
|
switch (ctx.kind) {
|
|
4331
5356
|
case "max_output_tokens":
|
|
4332
5357
|
return [
|
|
4333
5358
|
`The model hit the per-request output cap (${ctx.maxOutputTokens} completion tokens). Long plans can stop mid-markdown even though the session is otherwise healthy.`,
|
|
4334
5359
|
"You can continue: squad-kit will ask the model to append the rest of the plan in a follow-up request (extra tokens apply).",
|
|
4335
|
-
|
|
5360
|
+
tokenSummary(ctx)
|
|
4336
5361
|
];
|
|
4337
5362
|
case "max_iterations":
|
|
4338
5363
|
return [
|
|
4339
5364
|
`The planner reached its round cap (${ctx.maxIterations} model turns) without a clean stop.`,
|
|
4340
5365
|
"Continuing raises the round cap and file/time budgets for this run by another full slice from your config.",
|
|
4341
|
-
|
|
5366
|
+
tokenSummary(ctx)
|
|
4342
5367
|
];
|
|
4343
5368
|
case "wall_clock":
|
|
4344
5369
|
return [
|
|
4345
5370
|
"The configured wall-clock budget for this planning session elapsed before the model finished.",
|
|
4346
5371
|
"Continuing adds another slice of time (and read/output/round limits) from your planner budget.",
|
|
4347
|
-
|
|
4348
|
-
];
|
|
4349
|
-
case "cost_cap":
|
|
4350
|
-
return [
|
|
4351
|
-
"Spending crossed the optional `planner.budget.maxCostUsd` ceiling for this session.",
|
|
4352
|
-
"Continuing raises that ceiling for this run by another slice from config (if a cap is set).",
|
|
4353
|
-
cost
|
|
5372
|
+
tokenSummary(ctx)
|
|
4354
5373
|
];
|
|
4355
5374
|
case "file_or_context_reads":
|
|
4356
5375
|
return [
|
|
4357
5376
|
"A `read_file` call could not run because the file-read count or total read-bytes budget was exhausted.",
|
|
4358
5377
|
"Continuing extends read, context, time, round, and output limits for this session so the model can read again or finish without reads.",
|
|
4359
|
-
|
|
5378
|
+
tokenSummary(ctx)
|
|
4360
5379
|
];
|
|
4361
5380
|
}
|
|
4362
5381
|
}
|
|
@@ -4380,14 +5399,17 @@ function printPlannerLimitNextSteps() {
|
|
|
4380
5399
|
}
|
|
4381
5400
|
|
|
4382
5401
|
// src/planner/providers/index.ts
|
|
4383
|
-
|
|
4384
|
-
|
|
5402
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
5403
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
5404
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
5405
|
+
function resolveModel(provider, modelId, apiKey) {
|
|
5406
|
+
switch (provider) {
|
|
4385
5407
|
case "anthropic":
|
|
4386
|
-
return
|
|
5408
|
+
return { provider, model: createAnthropic({ apiKey })(modelId), modelId };
|
|
4387
5409
|
case "openai":
|
|
4388
|
-
return
|
|
5410
|
+
return { provider, model: createOpenAI({ apiKey })(modelId), modelId };
|
|
4389
5411
|
case "google":
|
|
4390
|
-
return
|
|
5412
|
+
return { provider, model: createGoogleGenerativeAI({ apiKey })(modelId), modelId };
|
|
4391
5413
|
}
|
|
4392
5414
|
}
|
|
4393
5415
|
|
|
@@ -4398,7 +5420,6 @@ var Budget = class {
|
|
|
4398
5420
|
this.maxFileReadsAllowed = cfg.maxFileReads;
|
|
4399
5421
|
this.maxContextBytesAllowed = cfg.maxContextBytes;
|
|
4400
5422
|
this.maxWallSeconds = cfg.maxDurationSeconds;
|
|
4401
|
-
this.maxCostUsdCeiling = cfg.maxCostUsd;
|
|
4402
5423
|
}
|
|
4403
5424
|
cfg;
|
|
4404
5425
|
reads = 0;
|
|
@@ -4408,15 +5429,11 @@ var Budget = class {
|
|
|
4408
5429
|
maxFileReadsAllowed;
|
|
4409
5430
|
maxContextBytesAllowed;
|
|
4410
5431
|
maxWallSeconds;
|
|
4411
|
-
maxCostUsdCeiling;
|
|
4412
5432
|
/** Add another slice of the original config limits (used when the user chooses to continue a planner session). */
|
|
4413
5433
|
extendSession() {
|
|
4414
5434
|
this.maxFileReadsAllowed += this.cfg.maxFileReads;
|
|
4415
5435
|
this.maxContextBytesAllowed += this.cfg.maxContextBytes;
|
|
4416
5436
|
this.maxWallSeconds += this.cfg.maxDurationSeconds;
|
|
4417
|
-
if (this.cfg.maxCostUsd !== void 0) {
|
|
4418
|
-
this.maxCostUsdCeiling = (this.maxCostUsdCeiling ?? 0) + this.cfg.maxCostUsd;
|
|
4419
|
-
}
|
|
4420
5437
|
}
|
|
4421
5438
|
canRead(nextFileBytes) {
|
|
4422
5439
|
if (this.reads >= this.maxFileReadsAllowed) {
|
|
@@ -4427,6 +5444,13 @@ var Budget = class {
|
|
|
4427
5444
|
}
|
|
4428
5445
|
return { ok: true };
|
|
4429
5446
|
}
|
|
5447
|
+
/** Whether another tool invocation is allowed under the read-count budget (bytes checked per tool). */
|
|
5448
|
+
canExecuteTool() {
|
|
5449
|
+
if (this.reads >= this.maxFileReadsAllowed) {
|
|
5450
|
+
return { ok: false, reason: `max file reads (${this.maxFileReadsAllowed}) reached` };
|
|
5451
|
+
}
|
|
5452
|
+
return { ok: true };
|
|
5453
|
+
}
|
|
4430
5454
|
recordRead(bytes) {
|
|
4431
5455
|
this.reads += 1;
|
|
4432
5456
|
this.bytes += bytes;
|
|
@@ -4434,17 +5458,12 @@ var Budget = class {
|
|
|
4434
5458
|
recordUsage(u) {
|
|
4435
5459
|
this.totalUsage.inputTokens += u.inputTokens;
|
|
4436
5460
|
this.totalUsage.outputTokens += u.outputTokens;
|
|
4437
|
-
if (u.costUsd !== void 0) this.totalUsage.costUsd = (this.totalUsage.costUsd ?? 0) + u.costUsd;
|
|
4438
5461
|
this.totalUsage.cacheCreationTokens = (this.totalUsage.cacheCreationTokens ?? 0) + (u.cacheCreationTokens ?? 0);
|
|
4439
5462
|
this.totalUsage.cacheReadTokens = (this.totalUsage.cacheReadTokens ?? 0) + (u.cacheReadTokens ?? 0);
|
|
4440
5463
|
}
|
|
4441
5464
|
timedOut() {
|
|
4442
5465
|
return (Date.now() - this.startedAt) / 1e3 > this.maxWallSeconds;
|
|
4443
5466
|
}
|
|
4444
|
-
overCost() {
|
|
4445
|
-
if (this.maxCostUsdCeiling === void 0) return false;
|
|
4446
|
-
return (this.totalUsage.costUsd ?? 0) > this.maxCostUsdCeiling;
|
|
4447
|
-
}
|
|
4448
5467
|
snapshot() {
|
|
4449
5468
|
return {
|
|
4450
5469
|
reads: this.reads,
|
|
@@ -4455,103 +5474,9 @@ var Budget = class {
|
|
|
4455
5474
|
}
|
|
4456
5475
|
};
|
|
4457
5476
|
|
|
4458
|
-
// src/core/repo-map.ts
|
|
4459
|
-
import fs23 from "fs";
|
|
4460
|
-
import path16 from "path";
|
|
4461
|
-
import ignore from "ignore";
|
|
4462
|
-
var DEFAULT_IGNORES = [
|
|
4463
|
-
"node_modules",
|
|
4464
|
-
".git",
|
|
4465
|
-
"dist",
|
|
4466
|
-
"build",
|
|
4467
|
-
"coverage",
|
|
4468
|
-
".next",
|
|
4469
|
-
".turbo",
|
|
4470
|
-
".cache",
|
|
4471
|
-
".squad/plans"
|
|
4472
|
-
// don't include existing plans as context; planner will see them separately
|
|
4473
|
-
];
|
|
4474
|
-
function buildRepoMap(root, opts = {}) {
|
|
4475
|
-
const maxEntries = opts.maxEntries ?? 5e3;
|
|
4476
|
-
const ig = buildIgnore(root);
|
|
4477
|
-
const entries = [];
|
|
4478
|
-
walk2(root, root, ig, entries, maxEntries, !!opts.includeHidden);
|
|
4479
|
-
entries.sort();
|
|
4480
|
-
return entries.join("\n") + "\n";
|
|
4481
|
-
}
|
|
4482
|
-
function buildIgnore(root) {
|
|
4483
|
-
const ig = ignore().add(DEFAULT_IGNORES);
|
|
4484
|
-
const gitignore = path16.join(root, ".gitignore");
|
|
4485
|
-
if (fs23.existsSync(gitignore)) {
|
|
4486
|
-
try {
|
|
4487
|
-
ig.add(fs23.readFileSync(gitignore, "utf8"));
|
|
4488
|
-
} catch {
|
|
4489
|
-
}
|
|
4490
|
-
}
|
|
4491
|
-
return ig;
|
|
4492
|
-
}
|
|
4493
|
-
function walk2(root, dir, ig, out, max, includeHidden) {
|
|
4494
|
-
if (out.length >= max) return;
|
|
4495
|
-
let entries;
|
|
4496
|
-
try {
|
|
4497
|
-
entries = fs23.readdirSync(dir, { withFileTypes: true });
|
|
4498
|
-
} catch {
|
|
4499
|
-
return;
|
|
4500
|
-
}
|
|
4501
|
-
for (const entry of entries) {
|
|
4502
|
-
if (out.length >= max) return;
|
|
4503
|
-
if (!includeHidden && entry.name.startsWith(".") && entry.name !== ".squad") continue;
|
|
4504
|
-
const abs = path16.join(dir, entry.name);
|
|
4505
|
-
const rel = path16.relative(root, abs);
|
|
4506
|
-
if (!rel || ig.ignores(rel) || entry.isDirectory() && ig.ignores(rel + "/")) continue;
|
|
4507
|
-
if (entry.isDirectory()) {
|
|
4508
|
-
walk2(root, abs, ig, out, max, includeHidden);
|
|
4509
|
-
} else if (entry.isFile()) {
|
|
4510
|
-
out.push(rel);
|
|
4511
|
-
}
|
|
4512
|
-
}
|
|
4513
|
-
}
|
|
4514
|
-
|
|
4515
|
-
// src/planner/system-prompt.ts
|
|
4516
|
-
function composeSystemPrompt(args) {
|
|
4517
|
-
const base = readBundledPrompt("generate-plan.md").replace(/\{\{projectRoots\}\}/g, args.projectRoots.join(", ")).replace(/\{\{primaryLanguage\}\}/g, args.primaryLanguage).replace(/\{\{trackerType\}\}/g, args.trackerType).replace(
|
|
4518
|
-
/\{\{intakeContent\}\}/g,
|
|
4519
|
-
"_(The intake story is provided in the user message below. Do not ask for it.)_"
|
|
4520
|
-
);
|
|
4521
|
-
const apiPreamble = [
|
|
4522
|
-
"",
|
|
4523
|
-
"---",
|
|
4524
|
-
"",
|
|
4525
|
-
"## Direct-API mode notes",
|
|
4526
|
-
"",
|
|
4527
|
-
"You are being invoked by the squad-kit CLI, not by a human editor. The intake is already inlined below.",
|
|
4528
|
-
"The repository tree is provided. To see the contents of any file, **call the `read_file` tool** with a repo-relative path.",
|
|
4529
|
-
"You have a bounded context budget (file reads, total bytes, wall-clock time). Read files you genuinely need.",
|
|
4530
|
-
"When you have enough information, output the **complete plan markdown** as your final assistant message. No prose around it, no code fences wrapping the whole plan. The CLI will write your final message verbatim to disk.",
|
|
4531
|
-
"",
|
|
4532
|
-
"## Repository tree",
|
|
4533
|
-
"",
|
|
4534
|
-
"```",
|
|
4535
|
-
args.repoMap.trimEnd(),
|
|
4536
|
-
"```",
|
|
4537
|
-
""
|
|
4538
|
-
].join("\n");
|
|
4539
|
-
return base + apiPreamble;
|
|
4540
|
-
}
|
|
4541
|
-
function composeUserPrompt(args) {
|
|
4542
|
-
return [
|
|
4543
|
-
"Produce the implementation plan for the following intake.",
|
|
4544
|
-
"",
|
|
4545
|
-
"---",
|
|
4546
|
-
"",
|
|
4547
|
-
args.intakeContent.trimEnd(),
|
|
4548
|
-
""
|
|
4549
|
-
].join("\n");
|
|
4550
|
-
}
|
|
4551
|
-
|
|
4552
5477
|
// src/planner/writer.ts
|
|
4553
|
-
import
|
|
4554
|
-
import
|
|
5478
|
+
import fs28 from "fs";
|
|
5479
|
+
import path20 from "path";
|
|
4555
5480
|
function planFileSlugParts(config2, story) {
|
|
4556
5481
|
const baseSlug = slugify(story.id);
|
|
4557
5482
|
const trackerId = config2.naming.includeTrackerId && config2.tracker.type !== "none" ? trackerIdForFilename(config2.tracker.type, story.id) : "";
|
|
@@ -4560,8 +5485,8 @@ function planFileSlugParts(config2, story) {
|
|
|
4560
5485
|
}
|
|
4561
5486
|
function writePlanFile(input5) {
|
|
4562
5487
|
const { paths, config: config2, story } = input5;
|
|
4563
|
-
const featurePlanDir =
|
|
4564
|
-
|
|
5488
|
+
const featurePlanDir = path20.join(paths.plansDir, story.feature);
|
|
5489
|
+
fs28.mkdirSync(featurePlanDir, { recursive: true });
|
|
4565
5490
|
if (input5.partial) {
|
|
4566
5491
|
return writePartialPlanFile(input5, featurePlanDir, paths, config2, story);
|
|
4567
5492
|
}
|
|
@@ -4576,14 +5501,14 @@ function writePlanFile(input5) {
|
|
|
4576
5501
|
} else {
|
|
4577
5502
|
nn = scanPlans(paths.plansDir).nextGlobal;
|
|
4578
5503
|
const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
|
|
4579
|
-
targetPath =
|
|
5504
|
+
targetPath = path20.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.md`);
|
|
4580
5505
|
}
|
|
4581
5506
|
const body = `${input5.metadataHeader.trimEnd()}
|
|
4582
5507
|
|
|
4583
5508
|
${input5.planBodyMarkdown.trimStart()}
|
|
4584
5509
|
`;
|
|
4585
|
-
|
|
4586
|
-
upsertOverviewRow(featurePlanDir, story.feature, nn,
|
|
5510
|
+
fs28.writeFileSync(targetPath, body, "utf8");
|
|
5511
|
+
upsertOverviewRow(featurePlanDir, story.feature, nn, path20.basename(targetPath), story, false);
|
|
4587
5512
|
return { planFile: targetPath, sequenceNumber: nn, overwrote };
|
|
4588
5513
|
}
|
|
4589
5514
|
function writePartialPlanFile(input5, featurePlanDir, paths, config2, story) {
|
|
@@ -4598,42 +5523,42 @@ function writePartialPlanFile(input5, featurePlanDir, paths, config2, story) {
|
|
|
4598
5523
|
} else {
|
|
4599
5524
|
nn = scanPlans(paths.plansDir).nextGlobal;
|
|
4600
5525
|
const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
|
|
4601
|
-
targetPath =
|
|
5526
|
+
targetPath = path20.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.partial.md`);
|
|
4602
5527
|
}
|
|
4603
5528
|
const body = `${input5.metadataHeader.trimEnd()}
|
|
4604
5529
|
|
|
4605
5530
|
${input5.planBodyMarkdown.trimStart()}
|
|
4606
5531
|
`;
|
|
4607
|
-
|
|
4608
|
-
upsertOverviewRow(featurePlanDir, story.feature, nn,
|
|
5532
|
+
fs28.writeFileSync(targetPath, body, "utf8");
|
|
5533
|
+
upsertOverviewRow(featurePlanDir, story.feature, nn, path20.basename(targetPath), story, true);
|
|
4609
5534
|
return { planFile: targetPath, sequenceNumber: nn, overwrote };
|
|
4610
5535
|
}
|
|
4611
5536
|
function findExistingPlan(featureDir, storyId) {
|
|
4612
|
-
if (!
|
|
5537
|
+
if (!fs28.existsSync(featureDir)) return void 0;
|
|
4613
5538
|
const pattern = /^(\d{2,})-story-.+\.md$/;
|
|
4614
|
-
for (const entry of
|
|
5539
|
+
for (const entry of fs28.readdirSync(featureDir)) {
|
|
4615
5540
|
if (entry.endsWith(".partial.md")) continue;
|
|
4616
5541
|
const m = entry.match(pattern);
|
|
4617
5542
|
if (!m) continue;
|
|
4618
|
-
if (entry.includes(storyId)) return { absPath:
|
|
5543
|
+
if (entry.includes(storyId)) return { absPath: path20.join(featureDir, entry), nn: parseInt(m[1], 10) };
|
|
4619
5544
|
}
|
|
4620
5545
|
return void 0;
|
|
4621
5546
|
}
|
|
4622
5547
|
function findExistingPartialPlan(featureDir, storyId) {
|
|
4623
|
-
if (!
|
|
5548
|
+
if (!fs28.existsSync(featureDir)) return void 0;
|
|
4624
5549
|
const pattern = /^(\d{2,})-story-.+\.partial\.md$/;
|
|
4625
|
-
for (const entry of
|
|
5550
|
+
for (const entry of fs28.readdirSync(featureDir)) {
|
|
4626
5551
|
const m = entry.match(pattern);
|
|
4627
5552
|
if (!m) continue;
|
|
4628
|
-
if (entry.includes(storyId)) return { absPath:
|
|
5553
|
+
if (entry.includes(storyId)) return { absPath: path20.join(featureDir, entry), nn: parseInt(m[1], 10) };
|
|
4629
5554
|
}
|
|
4630
5555
|
return void 0;
|
|
4631
5556
|
}
|
|
4632
5557
|
function upsertOverviewRow(featureDir, feature, nn, filename, story, isPartial) {
|
|
4633
|
-
const overviewPath =
|
|
5558
|
+
const overviewPath = path20.join(featureDir, "00-overview.md");
|
|
4634
5559
|
const titleCell = isPartial ? `${story.titleHint ?? story.id} (partial)` : story.titleHint ?? story.id;
|
|
4635
5560
|
const row = `| ${formatSequence(nn)} | \`${filename}\` | ${titleCell} | ${story.id} | \u2014 |`;
|
|
4636
|
-
if (!
|
|
5561
|
+
if (!fs28.existsSync(overviewPath)) {
|
|
4637
5562
|
const content = `# ${feature} \u2014 plan overview
|
|
4638
5563
|
|
|
4639
5564
|
Entry point for the **${feature}** feature. Stories execute in order by their \`NN\` prefix.
|
|
@@ -4644,10 +5569,10 @@ Entry point for the **${feature}** feature. Stories execute in order by their \`
|
|
|
4644
5569
|
|----|------|-------|------------|------------|
|
|
4645
5570
|
${row}
|
|
4646
5571
|
`;
|
|
4647
|
-
|
|
5572
|
+
fs28.writeFileSync(overviewPath, content, "utf8");
|
|
4648
5573
|
return;
|
|
4649
5574
|
}
|
|
4650
|
-
const current =
|
|
5575
|
+
const current = fs28.readFileSync(overviewPath, "utf8");
|
|
4651
5576
|
const lines = current.split("\n");
|
|
4652
5577
|
const existingIdx = lines.findIndex((l) => l.includes(`\`${filename}\``) || l.includes(`| ${formatSequence(nn)} |`));
|
|
4653
5578
|
if (existingIdx >= 0) {
|
|
@@ -4656,7 +5581,7 @@ ${row}
|
|
|
4656
5581
|
const tableEnd = findTableEnd(lines);
|
|
4657
5582
|
lines.splice(tableEnd, 0, row);
|
|
4658
5583
|
}
|
|
4659
|
-
|
|
5584
|
+
fs28.writeFileSync(overviewPath, lines.join("\n"), "utf8");
|
|
4660
5585
|
}
|
|
4661
5586
|
function findTableEnd(lines) {
|
|
4662
5587
|
let inTable = false;
|
|
@@ -4675,9 +5600,17 @@ function buildMetadataHeader(args) {
|
|
|
4675
5600
|
`generated by ${args.provider}/${args.model}`,
|
|
4676
5601
|
`at ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
4677
5602
|
`${args.reads} reads / ${(args.bytes / 1024).toFixed(1)} KB context`,
|
|
4678
|
-
`${args.inputTokens} in / ${args.outputTokens} out tokens
|
|
4679
|
-
args.costUsd !== void 0 ? `~$${args.costUsd.toFixed(2)}` : null
|
|
5603
|
+
`${args.inputTokens} in / ${args.outputTokens} out tokens`
|
|
4680
5604
|
].filter(Boolean);
|
|
5605
|
+
if (args.scoutEnabled !== void 0) {
|
|
5606
|
+
parts.push(`scout=${args.scoutEnabled ? "on" : "off"}`);
|
|
5607
|
+
}
|
|
5608
|
+
if (args.validationEnabled !== void 0) {
|
|
5609
|
+
parts.push(`validation=${args.validationEnabled ? "on" : "off"}`);
|
|
5610
|
+
}
|
|
5611
|
+
if (args.validationIssueCount !== void 0) {
|
|
5612
|
+
parts.push(`issues=${args.validationIssueCount}`);
|
|
5613
|
+
}
|
|
4681
5614
|
const line1 = `<!-- squad-kit: ${parts.join(", ")} -->`;
|
|
4682
5615
|
if (args.planStatus === "partial") {
|
|
4683
5616
|
return `${line1}
|
|
@@ -4701,16 +5634,16 @@ function buildCopyPlanPromptMarkdown(config2, intakeContent) {
|
|
|
4701
5634
|
}
|
|
4702
5635
|
|
|
4703
5636
|
// src/core/last-run.ts
|
|
4704
|
-
import
|
|
4705
|
-
import
|
|
5637
|
+
import fs29 from "fs/promises";
|
|
5638
|
+
import path21 from "path";
|
|
4706
5639
|
async function writeLastRun(paths, rec) {
|
|
4707
|
-
const file =
|
|
4708
|
-
await
|
|
5640
|
+
const file = path21.join(paths.squadDir, ".last-run.json");
|
|
5641
|
+
await fs29.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
|
|
4709
5642
|
}
|
|
4710
5643
|
async function readLastRun(paths) {
|
|
4711
|
-
const file =
|
|
5644
|
+
const file = path21.join(paths.squadDir, ".last-run.json");
|
|
4712
5645
|
try {
|
|
4713
|
-
const raw = await
|
|
5646
|
+
const raw = await fs29.readFile(file, "utf8");
|
|
4714
5647
|
const parsed = JSON.parse(raw);
|
|
4715
5648
|
if (parsed.version !== 1) return void 0;
|
|
4716
5649
|
return parsed;
|
|
@@ -4739,6 +5672,59 @@ function formatPlannerCacheLine(opts) {
|
|
|
4739
5672
|
return "cache miss 0% \u2014 run `squad doctor` to diagnose";
|
|
4740
5673
|
}
|
|
4741
5674
|
|
|
5675
|
+
// src/core/plans-index.ts
|
|
5676
|
+
import fs30 from "fs";
|
|
5677
|
+
import path22 from "path";
|
|
5678
|
+
var MAX_ENTRIES = 30;
|
|
5679
|
+
function firstMarkdownH1(src) {
|
|
5680
|
+
const lines = src.split(/\r?\n/);
|
|
5681
|
+
for (const line2 of lines) {
|
|
5682
|
+
const m = /^\s*#\s+(.+?)\s*$/.exec(line2);
|
|
5683
|
+
if (m) return m[1].trim();
|
|
5684
|
+
}
|
|
5685
|
+
return void 0;
|
|
5686
|
+
}
|
|
5687
|
+
function collectPlanMarkdownFiles(plansDir) {
|
|
5688
|
+
const out = [];
|
|
5689
|
+
const walk2 = (dir) => {
|
|
5690
|
+
let ents;
|
|
5691
|
+
try {
|
|
5692
|
+
ents = fs30.readdirSync(dir, { withFileTypes: true });
|
|
5693
|
+
} catch {
|
|
5694
|
+
return;
|
|
5695
|
+
}
|
|
5696
|
+
for (const ent of ents) {
|
|
5697
|
+
const abs = path22.join(dir, ent.name);
|
|
5698
|
+
if (ent.isDirectory()) {
|
|
5699
|
+
walk2(abs);
|
|
5700
|
+
} else if (ent.isFile() && ent.name.endsWith(".md")) {
|
|
5701
|
+
if (ent.name === "00-index.md" || ent.name === "00-overview.md") continue;
|
|
5702
|
+
out.push(abs);
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5705
|
+
};
|
|
5706
|
+
if (fs30.existsSync(plansDir)) walk2(plansDir);
|
|
5707
|
+
out.sort((a, b) => a.localeCompare(b));
|
|
5708
|
+
return out.slice(0, MAX_ENTRIES);
|
|
5709
|
+
}
|
|
5710
|
+
function buildPlansIndex(paths) {
|
|
5711
|
+
const files = collectPlanMarkdownFiles(paths.plansDir);
|
|
5712
|
+
if (files.length === 0) return "(no prior plans yet)";
|
|
5713
|
+
const bullets = [];
|
|
5714
|
+
for (const abs of files) {
|
|
5715
|
+
const rel = path22.relative(paths.root, abs).split(path22.sep).join("/");
|
|
5716
|
+
let title = "";
|
|
5717
|
+
try {
|
|
5718
|
+
const raw = fs30.readFileSync(abs, "utf8");
|
|
5719
|
+
title = firstMarkdownH1(raw) ?? path22.basename(abs);
|
|
5720
|
+
} catch {
|
|
5721
|
+
title = path22.basename(abs);
|
|
5722
|
+
}
|
|
5723
|
+
bullets.push(`- ${rel} \u2014 "${title}"`);
|
|
5724
|
+
}
|
|
5725
|
+
return bullets.join("\n");
|
|
5726
|
+
}
|
|
5727
|
+
|
|
4742
5728
|
// src/commands/new-plan.ts
|
|
4743
5729
|
function decideMode(opts, config2) {
|
|
4744
5730
|
if (opts.api && opts.copy) {
|
|
@@ -4770,8 +5756,8 @@ async function runNewPlan(intakePath, opts) {
|
|
|
4770
5756
|
}
|
|
4771
5757
|
}
|
|
4772
5758
|
function resolveFromPath(intakePath, stories, root) {
|
|
4773
|
-
const resolved =
|
|
4774
|
-
if (!
|
|
5759
|
+
const resolved = path23.resolve(intakePath.trim());
|
|
5760
|
+
if (!fs31.existsSync(resolved)) {
|
|
4775
5761
|
throw new Error(
|
|
4776
5762
|
`Intake not found: ${resolved}. Run \`squad list\` to see intakes, or \`squad new-story\` to create one first.`
|
|
4777
5763
|
);
|
|
@@ -4783,7 +5769,7 @@ function resolveFromPath(intakePath, stories, root) {
|
|
|
4783
5769
|
);
|
|
4784
5770
|
}
|
|
4785
5771
|
function relativeStoriesDir(root) {
|
|
4786
|
-
return
|
|
5772
|
+
return path23.relative(root, path23.join(root, ".squad", "stories"));
|
|
4787
5773
|
}
|
|
4788
5774
|
async function pickStory(stories, opts) {
|
|
4789
5775
|
const candidates = opts.all ? stories : stories.filter((s) => !s.planFile);
|
|
@@ -4814,7 +5800,7 @@ async function pickStory(stories, opts) {
|
|
|
4814
5800
|
value: s.intakePath
|
|
4815
5801
|
}))
|
|
4816
5802
|
});
|
|
4817
|
-
return candidates.find((s) =>
|
|
5803
|
+
return candidates.find((s) => path23.resolve(s.intakePath) === path23.resolve(pick));
|
|
4818
5804
|
}
|
|
4819
5805
|
function formatLabel(s) {
|
|
4820
5806
|
const head = `${s.feature} / ${s.id}`;
|
|
@@ -4860,13 +5846,13 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4860
5846
|
`Missing ${providerEnvVar(planner.provider)}. Run \`squad config set planner\` to save a key to secrets, or export the env var, or run \`squad new-plan --copy\` without the API.`
|
|
4861
5847
|
);
|
|
4862
5848
|
}
|
|
4863
|
-
const
|
|
4864
|
-
const
|
|
5849
|
+
const modelId = modelFor(planner.provider, "plan", planner.modelOverride);
|
|
5850
|
+
const { model } = resolveModel(planner.provider, modelId, apiKey);
|
|
4865
5851
|
const budget = new Budget(planner.budget);
|
|
4866
5852
|
banner();
|
|
4867
5853
|
step(`planning ${story.feature} / ${story.id}`);
|
|
4868
5854
|
kv("provider", planner.provider);
|
|
4869
|
-
kv("model",
|
|
5855
|
+
kv("model", modelId);
|
|
4870
5856
|
kv(
|
|
4871
5857
|
"budget",
|
|
4872
5858
|
`${planner.budget.maxFileReads} reads \xB7 ${(planner.budget.maxContextBytes / 1024).toFixed(0)} KB \xB7 ${planner.budget.maxDurationSeconds}s`
|
|
@@ -4877,7 +5863,7 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4877
5863
|
printPlannerApiCostNotice();
|
|
4878
5864
|
}
|
|
4879
5865
|
const mapSpinner = spinner("building repo map\u2026");
|
|
4880
|
-
const repoMap = buildRepoMap(paths.root);
|
|
5866
|
+
const repoMap = buildRepoMap(paths.root, { format: "tree" });
|
|
4881
5867
|
mapSpinner.succeed(
|
|
4882
5868
|
`repo map ready (${repoMap.split("\n").length - 1} paths \xB7 ${(repoMap.length / 1024).toFixed(1)} KB)`
|
|
4883
5869
|
);
|
|
@@ -4889,6 +5875,29 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4889
5875
|
repoMap
|
|
4890
5876
|
});
|
|
4891
5877
|
const userPrompt = composeUserPrompt({ intakeContent: readFile(story.intakePath) });
|
|
5878
|
+
const scoutEnabled = !opts.noScout && planner.stages?.scout?.enabled !== false;
|
|
5879
|
+
const validationEnabled = !opts.noValidation && planner.validation?.enabled !== false;
|
|
5880
|
+
const strictValidation = !!opts.strictValidation || planner.validation?.strict === true;
|
|
5881
|
+
const maxScoutFiles = opts.maxScoutFiles ?? planner.stages?.scout?.maxFiles ?? 12;
|
|
5882
|
+
let scoutModelId = "";
|
|
5883
|
+
let scoutModelHandle;
|
|
5884
|
+
let scoutSystemPrompt;
|
|
5885
|
+
if (scoutEnabled) {
|
|
5886
|
+
scoutModelId = modelFor(
|
|
5887
|
+
planner.provider,
|
|
5888
|
+
"scout",
|
|
5889
|
+
planner.modelOverride,
|
|
5890
|
+
opts.scoutModel ?? planner.stages?.scout?.modelOverride
|
|
5891
|
+
);
|
|
5892
|
+
scoutModelHandle = resolveModel(planner.provider, scoutModelId, apiKey).model;
|
|
5893
|
+
scoutSystemPrompt = composeScoutSystemPrompt({
|
|
5894
|
+
projectRoots: config2.project.projectRoots ?? ["."],
|
|
5895
|
+
primaryLanguage: config2.project.primaryLanguage ?? "",
|
|
5896
|
+
trackerType: config2.tracker.type,
|
|
5897
|
+
repoMap,
|
|
5898
|
+
plansIndex: buildPlansIndex(paths)
|
|
5899
|
+
});
|
|
5900
|
+
}
|
|
4892
5901
|
const sessionSpinner = { current: null };
|
|
4893
5902
|
const startedAt = Date.now();
|
|
4894
5903
|
const cacheEnabled = planner.cache?.enabled ?? true;
|
|
@@ -4897,11 +5906,43 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4897
5906
|
const unsubscribe = bus.subscribe((e) => {
|
|
4898
5907
|
switch (e.kind) {
|
|
4899
5908
|
case "tool_call": {
|
|
4900
|
-
const
|
|
5909
|
+
const name = e.toolCall.name;
|
|
5910
|
+
const inp = e.toolCall.input;
|
|
5911
|
+
let label;
|
|
5912
|
+
if (name === "read_file") {
|
|
5913
|
+
label = `read ${String(inp.path ?? "<unknown>")}`;
|
|
5914
|
+
} else if (name === "grep") {
|
|
5915
|
+
label = `grep ${String(inp.pattern ?? "?").slice(0, 48)}`;
|
|
5916
|
+
} else if (name === "list_dir") {
|
|
5917
|
+
label = `list_dir ${String(inp.path ?? "?")}`;
|
|
5918
|
+
} else {
|
|
5919
|
+
label = name;
|
|
5920
|
+
}
|
|
4901
5921
|
sessionSpinner.current?.succeed(
|
|
4902
|
-
|
|
5922
|
+
`${label} (${(e.bytesLoaded / 1024).toFixed(1)} KB \xB7 ${(e.totalBytes / 1024).toFixed(1)} KB / ${(planner.budget.maxContextBytes / 1024).toFixed(0)} KB)`
|
|
4903
5923
|
);
|
|
4904
|
-
sessionSpinner.current = spinner("
|
|
5924
|
+
sessionSpinner.current = spinner("next tool\u2026");
|
|
5925
|
+
break;
|
|
5926
|
+
}
|
|
5927
|
+
case "stage_started": {
|
|
5928
|
+
sessionSpinner.current?.stop();
|
|
5929
|
+
if (e.stage === "scout") sessionSpinner.current = spinner("scouting\u2026");
|
|
5930
|
+
else if (e.stage === "draft") sessionSpinner.current = spinner("drafting\u2026");
|
|
5931
|
+
else if (e.stage === "validation") sessionSpinner.current = spinner("validating\u2026");
|
|
5932
|
+
break;
|
|
5933
|
+
}
|
|
5934
|
+
case "stage_complete": {
|
|
5935
|
+
if (!e.success && e.errorMessage && e.stage === "scout") {
|
|
5936
|
+
sessionSpinner.current?.stop();
|
|
5937
|
+
warning(`scout stage failed: ${e.errorMessage}`);
|
|
5938
|
+
}
|
|
5939
|
+
break;
|
|
5940
|
+
}
|
|
5941
|
+
case "scout_result": {
|
|
5942
|
+
const preview = e.selected.slice(0, 4).join(", ");
|
|
5943
|
+
const more = e.selected.length > 4 ? "\u2026" : "";
|
|
5944
|
+
sessionSpinner.current?.succeed(`scout picked ${e.selected.length} files: ${preview}${more}`);
|
|
5945
|
+
sessionSpinner.current = spinner("drafting\u2026");
|
|
4905
5946
|
break;
|
|
4906
5947
|
}
|
|
4907
5948
|
case "assistant_text":
|
|
@@ -4937,9 +5978,9 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4937
5978
|
try {
|
|
4938
5979
|
result = await runPlanner({
|
|
4939
5980
|
root: paths.root,
|
|
4940
|
-
provider,
|
|
4941
5981
|
model,
|
|
4942
|
-
|
|
5982
|
+
provider: planner.provider,
|
|
5983
|
+
modelId,
|
|
4943
5984
|
systemPrompt,
|
|
4944
5985
|
userPrompt,
|
|
4945
5986
|
budget,
|
|
@@ -4947,7 +5988,19 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4947
5988
|
cacheEnabled,
|
|
4948
5989
|
decideOnLimit,
|
|
4949
5990
|
events: bus,
|
|
4950
|
-
runId
|
|
5991
|
+
runId,
|
|
5992
|
+
stages: {
|
|
5993
|
+
scout: {
|
|
5994
|
+
enabled: scoutEnabled,
|
|
5995
|
+
model: scoutModelHandle,
|
|
5996
|
+
modelId: scoutModelId,
|
|
5997
|
+
maxFiles: maxScoutFiles,
|
|
5998
|
+
maxOutputTokens: planner.stages?.scout?.maxOutputTokens ?? 2048
|
|
5999
|
+
}
|
|
6000
|
+
},
|
|
6001
|
+
validation: { enabled: validationEnabled, strict: strictValidation },
|
|
6002
|
+
toolsEnabled: planner.tools,
|
|
6003
|
+
scoutSystemPrompt
|
|
4951
6004
|
});
|
|
4952
6005
|
} finally {
|
|
4953
6006
|
unsubscribe();
|
|
@@ -4960,17 +6013,22 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4960
6013
|
}
|
|
4961
6014
|
const snap = budget.snapshot();
|
|
4962
6015
|
const elapsedMs = Date.now() - startedAt;
|
|
4963
|
-
const
|
|
6016
|
+
const issueCount = result.validation?.issues.length ?? 0;
|
|
6017
|
+
const issuesByKind = summariseIssuesByKind(result.validation?.issues ?? []);
|
|
6018
|
+
const validationBlocks = strictValidation && validationEnabled && issueCount > 0;
|
|
6019
|
+
const success2 = result.finishedNormally && !result.timedOut && !result.userCancelled && !validationBlocks;
|
|
4964
6020
|
const header = buildMetadataHeader({
|
|
4965
6021
|
provider: planner.provider,
|
|
4966
|
-
model,
|
|
6022
|
+
model: modelId,
|
|
4967
6023
|
reads: snap.reads,
|
|
4968
6024
|
bytes: snap.bytes,
|
|
4969
6025
|
inputTokens: snap.usage.inputTokens,
|
|
4970
6026
|
outputTokens: snap.usage.outputTokens,
|
|
4971
6027
|
durationMs: elapsedMs,
|
|
4972
|
-
|
|
4973
|
-
|
|
6028
|
+
planStatus: success2 ? void 0 : "partial",
|
|
6029
|
+
scoutEnabled,
|
|
6030
|
+
validationEnabled,
|
|
6031
|
+
validationIssueCount: issueCount
|
|
4974
6032
|
});
|
|
4975
6033
|
const { planFile, sequenceNumber, overwrote } = writePlanFile({
|
|
4976
6034
|
paths,
|
|
@@ -4980,18 +6038,18 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4980
6038
|
metadataHeader: header,
|
|
4981
6039
|
partial: !success2
|
|
4982
6040
|
});
|
|
4983
|
-
const relPlan =
|
|
6041
|
+
const relPlan = path23.relative(paths.root, planFile);
|
|
4984
6042
|
try {
|
|
4985
6043
|
await writeLastRun(paths, {
|
|
4986
6044
|
stats: result.stats,
|
|
4987
6045
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4988
6046
|
provider: planner.provider,
|
|
4989
|
-
model
|
|
6047
|
+
model: modelId
|
|
4990
6048
|
});
|
|
4991
6049
|
await appendRun(paths, {
|
|
4992
6050
|
runId,
|
|
4993
6051
|
provider: planner.provider,
|
|
4994
|
-
model,
|
|
6052
|
+
model: modelId,
|
|
4995
6053
|
feature: story.feature,
|
|
4996
6054
|
storyId: story.id,
|
|
4997
6055
|
startedAt: new Date(startedAt).toISOString(),
|
|
@@ -5001,7 +6059,19 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
5001
6059
|
planFile: relPlan,
|
|
5002
6060
|
stats: result.stats,
|
|
5003
6061
|
cacheEnabled,
|
|
5004
|
-
durationMs: elapsedMs
|
|
6062
|
+
durationMs: elapsedMs,
|
|
6063
|
+
scout: {
|
|
6064
|
+
enabled: scoutEnabled,
|
|
6065
|
+
selectedCount: result.scout?.selected.length,
|
|
6066
|
+
tokensUsed: result.scout?.tokensUsed,
|
|
6067
|
+
durationMs: result.scout?.durationMs
|
|
6068
|
+
},
|
|
6069
|
+
validation: {
|
|
6070
|
+
enabled: validationEnabled,
|
|
6071
|
+
issuesCount: issueCount,
|
|
6072
|
+
issuesByKind,
|
|
6073
|
+
durationMs: result.validation?.durationMs
|
|
6074
|
+
}
|
|
5005
6075
|
});
|
|
5006
6076
|
} catch {
|
|
5007
6077
|
}
|
|
@@ -5009,11 +6079,19 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
5009
6079
|
summaryBox(success2 ? " plan generated " : " plan saved (incomplete) ", [
|
|
5010
6080
|
{ key: "file", value: relPlan },
|
|
5011
6081
|
{ key: "nn", value: String(sequenceNumber).padStart(2, "0") },
|
|
5012
|
-
{ key: "model", value: `${planner.provider}/${
|
|
6082
|
+
{ key: "model", value: `${planner.provider}/${modelId}` },
|
|
6083
|
+
{
|
|
6084
|
+
key: "scout",
|
|
6085
|
+
value: scoutEnabled ? `${result.scout?.selected.length ?? 0} files \xB7 ${result.scout?.tokensUsed ?? 0} tok` : "disabled"
|
|
6086
|
+
},
|
|
6087
|
+
{
|
|
6088
|
+
key: "validation",
|
|
6089
|
+
value: validationEnabled ? `${issueCount} issue(s)` : "disabled"
|
|
6090
|
+
},
|
|
5013
6091
|
{ key: "reads", value: `${snap.reads} files \xB7 ${(snap.bytes / 1024).toFixed(1)} KB` },
|
|
5014
6092
|
{
|
|
5015
6093
|
key: "tokens",
|
|
5016
|
-
value: `${snap.usage.inputTokens} in \xB7 ${snap.usage.outputTokens} out
|
|
6094
|
+
value: `${snap.usage.inputTokens} in \xB7 ${snap.usage.outputTokens} out`
|
|
5017
6095
|
},
|
|
5018
6096
|
{ key: "cache", value: formatPlannerCacheLine({ cacheEnabled, stats: result.stats }) },
|
|
5019
6097
|
{
|
|
@@ -5023,25 +6101,45 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
5023
6101
|
{ key: "time", value: `${Math.round(elapsedMs / 1e3)}s` },
|
|
5024
6102
|
{
|
|
5025
6103
|
key: "action",
|
|
5026
|
-
value: success2 ? overwrote ? "overwrote existing plan" : "new plan" : "partial plan (review limits)"
|
|
6104
|
+
value: success2 ? overwrote ? "overwrote existing plan" : "new plan" : validationBlocks ? "partial plan (validation issues \u2014 review or use --no-validation)" : "partial plan (review limits)"
|
|
5027
6105
|
}
|
|
5028
6106
|
]);
|
|
6107
|
+
if (issueCount > 0 && validationEnabled) {
|
|
6108
|
+
blank();
|
|
6109
|
+
warning(`Validation reported ${issueCount} issue(s) (heuristic; first 10 shown):`);
|
|
6110
|
+
for (const iss of result.validation.issues.slice(0, 10)) {
|
|
6111
|
+
info(
|
|
6112
|
+
` [${iss.severity}] ${iss.kind}${iss.path ? ` ${iss.path}` : ""} \u2014 ${iss.detail}`
|
|
6113
|
+
);
|
|
6114
|
+
}
|
|
6115
|
+
if (issueCount > 10) {
|
|
6116
|
+
info(` (+${issueCount - 10} more \u2014 see metadata header issues=\u2026)`);
|
|
6117
|
+
}
|
|
6118
|
+
if (issueCount > 50) {
|
|
6119
|
+
warning("Large issue count may mean the validator is misfiring; try `squad new-plan --no-validation` to compare.");
|
|
6120
|
+
}
|
|
6121
|
+
}
|
|
5029
6122
|
if (!success2) {
|
|
5030
|
-
|
|
6123
|
+
if (!validationBlocks || !result.finishedNormally) {
|
|
6124
|
+
printPlannerRunIncompleteSummary(result, relPlan);
|
|
6125
|
+
} else {
|
|
6126
|
+
blank();
|
|
6127
|
+
warning("Plan saved as partial because strict validation reported issues.");
|
|
6128
|
+
}
|
|
5031
6129
|
throw new SquadExit(2);
|
|
5032
6130
|
}
|
|
5033
6131
|
if (result.budgetExhausted) {
|
|
5034
6132
|
info("Note: file-read budget was reached; the model finalised without further reads.");
|
|
5035
6133
|
}
|
|
5036
6134
|
blank();
|
|
5037
|
-
info(`next \u2192 open a new agent chat and attach only ${
|
|
6135
|
+
info(`next \u2192 open a new agent chat and attach only ${path23.basename(planFile)}`);
|
|
5038
6136
|
}
|
|
5039
6137
|
async function emitCopyPrompt(story, paths, config2, clipboard) {
|
|
5040
6138
|
const intakeContent = readFile(story.intakePath);
|
|
5041
6139
|
const composed = buildCopyPlanPromptMarkdown(config2, intakeContent);
|
|
5042
|
-
const promptFile =
|
|
5043
|
-
|
|
5044
|
-
const relPromptFile =
|
|
6140
|
+
const promptFile = path23.join(paths.squadDir, ".last-copy-prompt.md");
|
|
6141
|
+
fs31.writeFileSync(promptFile, composed, "utf8");
|
|
6142
|
+
const relPromptFile = path23.relative(paths.root, promptFile);
|
|
5045
6143
|
const bytes = Buffer.byteLength(composed, "utf8");
|
|
5046
6144
|
const estTokens = Math.round(bytes / 4);
|
|
5047
6145
|
const clipResult = clipboard ? await copyToClipboard(composed) : { ok: false, reason: "clipboard disabled (--no-clipboard)" };
|
|
@@ -5099,17 +6197,17 @@ function printModelBanner(config2) {
|
|
|
5099
6197
|
}
|
|
5100
6198
|
|
|
5101
6199
|
// src/commands/status.ts
|
|
5102
|
-
import
|
|
6200
|
+
import fs32 from "fs";
|
|
5103
6201
|
var kw = 28;
|
|
5104
6202
|
function probeTrackerCredential(config2, overlay) {
|
|
5105
|
-
const { client, error
|
|
6203
|
+
const { client, error } = clientFor(config2, overlay);
|
|
5106
6204
|
if (client) {
|
|
5107
6205
|
const fromEnv = config2.tracker.type === "jira" && Boolean(process.env.JIRA_API_TOKEN || process.env.SQUAD_TRACKER_API_KEY) || config2.tracker.type === "azure" && Boolean(process.env.AZURE_DEVOPS_PAT || process.env.SQUAD_TRACKER_API_KEY);
|
|
5108
6206
|
return { state: "present", detail: fromEnv ? "env" : "secrets.yaml" };
|
|
5109
6207
|
}
|
|
5110
6208
|
return {
|
|
5111
6209
|
state: "missing",
|
|
5112
|
-
hint:
|
|
6210
|
+
hint: error?.detail ?? "Run `squad init` to enter credentials."
|
|
5113
6211
|
};
|
|
5114
6212
|
}
|
|
5115
6213
|
function formatCredentialStatus(s) {
|
|
@@ -5125,7 +6223,7 @@ async function runStatus() {
|
|
|
5125
6223
|
const scan = scanPlans(paths.plansDir);
|
|
5126
6224
|
const storyCount = listStories(paths).length;
|
|
5127
6225
|
const planCount = scan.usedNumbers.length;
|
|
5128
|
-
const secrets =
|
|
6226
|
+
const secrets = fs32.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
5129
6227
|
const overlay = overlayTrackerEnv(secrets);
|
|
5130
6228
|
banner();
|
|
5131
6229
|
step("squad-kit status");
|
|
@@ -5179,13 +6277,13 @@ function buildTrackerLine(config2, overlay) {
|
|
|
5179
6277
|
}
|
|
5180
6278
|
|
|
5181
6279
|
// src/commands/list.ts
|
|
5182
|
-
import
|
|
6280
|
+
import path24 from "path";
|
|
5183
6281
|
async function runList(opts) {
|
|
5184
6282
|
const root = requireSquadRoot();
|
|
5185
6283
|
const paths = buildPaths(root);
|
|
5186
6284
|
const records = listStories(paths, { feature: opts.feature });
|
|
5187
6285
|
const rows = records.map((r) => {
|
|
5188
|
-
const planAbs = r.planFile ?
|
|
6286
|
+
const planAbs = r.planFile ? path24.join(paths.plansDir, r.feature, r.planFile) : void 0;
|
|
5189
6287
|
const meta = planAbs ? readPlanMetadata(planAbs) : void 0;
|
|
5190
6288
|
return {
|
|
5191
6289
|
feature: r.feature,
|
|
@@ -5224,8 +6322,8 @@ function stripAnsi(s) {
|
|
|
5224
6322
|
}
|
|
5225
6323
|
|
|
5226
6324
|
// src/commands/tracker-link.ts
|
|
5227
|
-
import
|
|
5228
|
-
import
|
|
6325
|
+
import fs33 from "fs";
|
|
6326
|
+
import path25 from "path";
|
|
5229
6327
|
import { input as input4, select as select7 } from "@inquirer/prompts";
|
|
5230
6328
|
async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
5231
6329
|
const root = requireSquadRoot();
|
|
@@ -5272,17 +6370,17 @@ async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
|
5272
6370
|
`Invalid tracker id "${resolvedTrackerId}" for tracker type "${config2.tracker.type}". Run \`squad config show\` to review the expected format, or \`squad config set tracker\` if the type is wrong.`
|
|
5273
6371
|
);
|
|
5274
6372
|
}
|
|
5275
|
-
const resolved =
|
|
5276
|
-
const intakeFile = resolved.endsWith("intake.md") ? resolved :
|
|
5277
|
-
if (!
|
|
6373
|
+
const resolved = path25.resolve(resolvedStoryPath);
|
|
6374
|
+
const intakeFile = resolved.endsWith("intake.md") ? resolved : path25.join(resolved, "intake.md");
|
|
6375
|
+
if (!fs33.existsSync(intakeFile)) {
|
|
5278
6376
|
throw new Error(
|
|
5279
6377
|
`intake.md not found at ${intakeFile}. Run \`squad list\` to see valid paths, or \`squad new-story\` to create a story first.`
|
|
5280
6378
|
);
|
|
5281
6379
|
}
|
|
5282
|
-
const content =
|
|
6380
|
+
const content = fs33.readFileSync(intakeFile, "utf8");
|
|
5283
6381
|
const updated = upsertTrackerId(content, resolvedTrackerId);
|
|
5284
|
-
|
|
5285
|
-
success(`Linked tracker id ${resolvedTrackerId} \u2192 ${
|
|
6382
|
+
fs33.writeFileSync(intakeFile, updated, "utf8");
|
|
6383
|
+
success(`Linked tracker id ${resolvedTrackerId} \u2192 ${path25.relative(root, intakeFile)}`);
|
|
5286
6384
|
blank();
|
|
5287
6385
|
step("Next:");
|
|
5288
6386
|
info("1) Review the intake \u2014 the Source block at the top now carries the tracker id.");
|
|
@@ -5300,39 +6398,39 @@ function upsertTrackerId(content, id) {
|
|
|
5300
6398
|
}
|
|
5301
6399
|
|
|
5302
6400
|
// src/commands/doctor-engine.ts
|
|
5303
|
-
import
|
|
5304
|
-
import
|
|
6401
|
+
import fs34 from "fs";
|
|
6402
|
+
import path26 from "path";
|
|
5305
6403
|
async function gatherContext(paths) {
|
|
5306
6404
|
const ctx = { hasLegacyPromptsDir: false };
|
|
5307
6405
|
try {
|
|
5308
6406
|
ctx.config = loadConfig(paths.configFile);
|
|
5309
|
-
} catch (
|
|
5310
|
-
ctx.configError =
|
|
6407
|
+
} catch (err2) {
|
|
6408
|
+
ctx.configError = err2;
|
|
5311
6409
|
}
|
|
5312
6410
|
try {
|
|
5313
|
-
ctx.secrets =
|
|
5314
|
-
} catch (
|
|
5315
|
-
ctx.secretsError =
|
|
6411
|
+
ctx.secrets = fs34.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
6412
|
+
} catch (err2) {
|
|
6413
|
+
ctx.secretsError = err2;
|
|
5316
6414
|
}
|
|
5317
|
-
ctx.hasLegacyPromptsDir =
|
|
6415
|
+
ctx.hasLegacyPromptsDir = fs34.existsSync(paths.promptsDir);
|
|
5318
6416
|
return ctx;
|
|
5319
6417
|
}
|
|
5320
6418
|
function gitignoreHasManagedBlock(repoRoot) {
|
|
5321
|
-
const gitignore =
|
|
5322
|
-
return
|
|
6419
|
+
const gitignore = path26.join(repoRoot, ".gitignore");
|
|
6420
|
+
return fs34.existsSync(gitignore) && fs34.readFileSync(gitignore, "utf8").includes(".squad/secrets.yaml");
|
|
5323
6421
|
}
|
|
5324
6422
|
function gitignoreHasTrashPattern(repoRoot) {
|
|
5325
|
-
const gitignore =
|
|
5326
|
-
return
|
|
6423
|
+
const gitignore = path26.join(repoRoot, ".gitignore");
|
|
6424
|
+
return fs34.existsSync(gitignore) && fs34.readFileSync(gitignore, "utf8").includes(SQUAD_TRASH_PATTERN);
|
|
5327
6425
|
}
|
|
5328
6426
|
async function checkDirStructure(paths, _ctx, fix) {
|
|
5329
|
-
const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !
|
|
6427
|
+
const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !fs34.existsSync(p));
|
|
5330
6428
|
if (need.length === 0) {
|
|
5331
6429
|
return { id: "dirs", name: ".squad/ directory structure", status: "ok" };
|
|
5332
6430
|
}
|
|
5333
6431
|
if (fix) {
|
|
5334
6432
|
for (const p of need) {
|
|
5335
|
-
|
|
6433
|
+
fs34.mkdirSync(p, { recursive: true });
|
|
5336
6434
|
}
|
|
5337
6435
|
return { id: "dirs", name: ".squad/ directory structure", status: "ok", detail: "repaired" };
|
|
5338
6436
|
}
|
|
@@ -5340,7 +6438,7 @@ async function checkDirStructure(paths, _ctx, fix) {
|
|
|
5340
6438
|
id: "dirs",
|
|
5341
6439
|
name: ".squad/ directory structure",
|
|
5342
6440
|
status: "warn",
|
|
5343
|
-
detail: `missing: ${need.map((p) =>
|
|
6441
|
+
detail: `missing: ${need.map((p) => path26.relative(paths.root, p)).join(", ")}`,
|
|
5344
6442
|
fixable: true,
|
|
5345
6443
|
fixHint: "squad doctor --fix"
|
|
5346
6444
|
};
|
|
@@ -5395,15 +6493,15 @@ async function checkSecretsPermissions(paths, _ctx, fix) {
|
|
|
5395
6493
|
if (process.platform === "win32") {
|
|
5396
6494
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "skip", detail: "Windows" };
|
|
5397
6495
|
}
|
|
5398
|
-
if (!
|
|
6496
|
+
if (!fs34.existsSync(paths.secretsFile)) {
|
|
5399
6497
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "not present" };
|
|
5400
6498
|
}
|
|
5401
|
-
const mode =
|
|
6499
|
+
const mode = fs34.statSync(paths.secretsFile).mode & 511;
|
|
5402
6500
|
if (mode === 384) {
|
|
5403
6501
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok" };
|
|
5404
6502
|
}
|
|
5405
6503
|
if (fix) {
|
|
5406
|
-
|
|
6504
|
+
fs34.chmodSync(paths.secretsFile, 384);
|
|
5407
6505
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "repaired" };
|
|
5408
6506
|
}
|
|
5409
6507
|
return {
|
|
@@ -5424,7 +6522,7 @@ async function checkSecretsParseable(_paths, ctx) {
|
|
|
5424
6522
|
detail: ctx.secretsError.message
|
|
5425
6523
|
};
|
|
5426
6524
|
}
|
|
5427
|
-
if (!
|
|
6525
|
+
if (!fs34.existsSync(_paths.secretsFile)) {
|
|
5428
6526
|
return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok", detail: "not present" };
|
|
5429
6527
|
}
|
|
5430
6528
|
return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok" };
|
|
@@ -5484,14 +6582,6 @@ async function checkPlannerConfig(_paths, ctx) {
|
|
|
5484
6582
|
detail: "planner budget limits must be > 0"
|
|
5485
6583
|
};
|
|
5486
6584
|
}
|
|
5487
|
-
if (p.budget.maxCostUsd !== void 0 && p.budget.maxCostUsd <= 0) {
|
|
5488
|
-
return {
|
|
5489
|
-
id: "planner-config",
|
|
5490
|
-
name: "planner configuration",
|
|
5491
|
-
status: "fail",
|
|
5492
|
-
detail: "planner.budget.maxCostUsd must be > 0 when set"
|
|
5493
|
-
};
|
|
5494
|
-
}
|
|
5495
6585
|
return { id: "planner-config", name: "planner configuration", status: "ok" };
|
|
5496
6586
|
}
|
|
5497
6587
|
async function checkPlannerCredential(_paths, ctx) {
|
|
@@ -5566,12 +6656,12 @@ async function checkPlannerModel(_paths, ctx) {
|
|
|
5566
6656
|
status: "ok",
|
|
5567
6657
|
detail: `${model} (${provider})`
|
|
5568
6658
|
};
|
|
5569
|
-
} catch (
|
|
6659
|
+
} catch (err2) {
|
|
5570
6660
|
return {
|
|
5571
6661
|
id: "planner-model",
|
|
5572
6662
|
name: "planner model resolves at provider",
|
|
5573
6663
|
status: "warn",
|
|
5574
|
-
detail:
|
|
6664
|
+
detail: err2.message
|
|
5575
6665
|
};
|
|
5576
6666
|
}
|
|
5577
6667
|
}
|
|
@@ -5704,8 +6794,8 @@ async function checkTrackerConfig(_paths, ctx) {
|
|
|
5704
6794
|
}
|
|
5705
6795
|
return { id: "tracker-config", name: "tracker configuration", status: "ok" };
|
|
5706
6796
|
}
|
|
5707
|
-
function formatClientError(
|
|
5708
|
-
return `${
|
|
6797
|
+
function formatClientError(err2) {
|
|
6798
|
+
return `${err2.message} ${err2.detail}`.trim();
|
|
5709
6799
|
}
|
|
5710
6800
|
async function checkTrackerCredential(_paths, ctx) {
|
|
5711
6801
|
if (!ctx.config) {
|
|
@@ -5716,13 +6806,13 @@ async function checkTrackerCredential(_paths, ctx) {
|
|
|
5716
6806
|
}
|
|
5717
6807
|
const secrets = ctx.secrets ?? {};
|
|
5718
6808
|
const overlay = overlayTrackerEnv(secrets);
|
|
5719
|
-
const { client, error
|
|
5720
|
-
if (
|
|
6809
|
+
const { client, error } = clientFor(ctx.config, overlay);
|
|
6810
|
+
if (error) {
|
|
5721
6811
|
return {
|
|
5722
6812
|
id: "tracker-cred",
|
|
5723
6813
|
name: "tracker credential resolves",
|
|
5724
6814
|
status: "fail",
|
|
5725
|
-
detail: formatClientError(
|
|
6815
|
+
detail: formatClientError(error)
|
|
5726
6816
|
};
|
|
5727
6817
|
}
|
|
5728
6818
|
if (!client) {
|
|
@@ -5741,8 +6831,8 @@ async function checkTrackerConnectivity(paths, ctx) {
|
|
|
5741
6831
|
}
|
|
5742
6832
|
const secrets = ctx.secrets ?? {};
|
|
5743
6833
|
const overlay = overlayTrackerEnv(secrets);
|
|
5744
|
-
const { client, error
|
|
5745
|
-
if (
|
|
6834
|
+
const { client, error } = clientFor(ctx.config, overlay);
|
|
6835
|
+
if (error || !client) {
|
|
5746
6836
|
return { id: "tracker-live", name: "tracker connectivity", status: "skip", detail: "no credential" };
|
|
5747
6837
|
}
|
|
5748
6838
|
if (client.name === "jira") {
|
|
@@ -5785,12 +6875,12 @@ async function checkTrackerConnectivity(paths, ctx) {
|
|
|
5785
6875
|
async function guardCheck(checkName, fn) {
|
|
5786
6876
|
try {
|
|
5787
6877
|
return await fn();
|
|
5788
|
-
} catch (
|
|
6878
|
+
} catch (err2) {
|
|
5789
6879
|
return {
|
|
5790
6880
|
id: "unexpected",
|
|
5791
6881
|
name: checkName,
|
|
5792
6882
|
status: "fail",
|
|
5793
|
-
detail:
|
|
6883
|
+
detail: err2.message
|
|
5794
6884
|
};
|
|
5795
6885
|
}
|
|
5796
6886
|
}
|
|
@@ -5918,23 +7008,23 @@ async function runDoctor(opts) {
|
|
|
5918
7008
|
}
|
|
5919
7009
|
|
|
5920
7010
|
// src/commands/migrate.ts
|
|
5921
|
-
import
|
|
5922
|
-
import
|
|
7011
|
+
import fs35 from "fs";
|
|
7012
|
+
import path27 from "path";
|
|
5923
7013
|
import { confirm as confirm7 } from "@inquirer/prompts";
|
|
5924
7014
|
var LEGACY_PROMPTS_MIGRATION = {
|
|
5925
7015
|
id: "legacy-prompts",
|
|
5926
7016
|
name: "Remove legacy .squad/prompts/",
|
|
5927
|
-
isApplied: (paths) => !
|
|
5928
|
-
describe: (paths) => `delete ${
|
|
7017
|
+
isApplied: (paths) => !fs35.existsSync(paths.promptsDir),
|
|
7018
|
+
describe: (paths) => `delete ${path27.relative(paths.root, paths.promptsDir)} (prompts are now bundled in squad-kit)`,
|
|
5929
7019
|
apply: (paths) => {
|
|
5930
7020
|
const target = paths.promptsDir;
|
|
5931
|
-
if (!target.includes(
|
|
7021
|
+
if (!target.includes(path27.sep + ".squad" + path27.sep + "prompts")) {
|
|
5932
7022
|
throw new Error(
|
|
5933
7023
|
`Refusing to delete suspicious path: ${target}. This is a safety check \u2014 run \`squad doctor\` if .squad/ paths look wrong.`
|
|
5934
7024
|
);
|
|
5935
7025
|
}
|
|
5936
|
-
if (
|
|
5937
|
-
|
|
7026
|
+
if (fs35.existsSync(target)) {
|
|
7027
|
+
fs35.rmSync(target, { recursive: true, force: true });
|
|
5938
7028
|
}
|
|
5939
7029
|
}
|
|
5940
7030
|
};
|
|
@@ -5942,8 +7032,8 @@ var GITIGNORE_MIGRATION = {
|
|
|
5942
7032
|
id: "gitignore",
|
|
5943
7033
|
name: "Ensure .gitignore managed block",
|
|
5944
7034
|
isApplied: (paths) => {
|
|
5945
|
-
const f =
|
|
5946
|
-
return
|
|
7035
|
+
const f = path27.join(paths.root, ".gitignore");
|
|
7036
|
+
return fs35.existsSync(f) && fs35.readFileSync(f, "utf8").includes(".squad/secrets.yaml");
|
|
5947
7037
|
},
|
|
5948
7038
|
describe: () => "add squad-kit managed block to .gitignore",
|
|
5949
7039
|
apply: (paths) => {
|
|
@@ -5955,23 +7045,23 @@ var SECRETS_PERMS_MIGRATION = {
|
|
|
5955
7045
|
name: "Tighten .squad/secrets.yaml permissions",
|
|
5956
7046
|
isApplied: (paths) => {
|
|
5957
7047
|
if (process.platform === "win32") return true;
|
|
5958
|
-
if (!
|
|
5959
|
-
const st =
|
|
7048
|
+
if (!fs35.existsSync(paths.secretsFile)) return true;
|
|
7049
|
+
const st = fs35.statSync(paths.secretsFile);
|
|
5960
7050
|
return (st.mode & 511) === 384;
|
|
5961
7051
|
},
|
|
5962
|
-
describe: (paths) => `chmod 0600 ${
|
|
7052
|
+
describe: (paths) => `chmod 0600 ${path27.relative(paths.root, paths.secretsFile)}`,
|
|
5963
7053
|
apply: (paths) => {
|
|
5964
7054
|
if (process.platform === "win32") return;
|
|
5965
|
-
if (
|
|
7055
|
+
if (fs35.existsSync(paths.secretsFile)) fs35.chmodSync(paths.secretsFile, 384);
|
|
5966
7056
|
}
|
|
5967
7057
|
};
|
|
5968
7058
|
var PLANNER_BUDGET_DEFAULTS_MIGRATION = {
|
|
5969
7059
|
id: "planner-budget-defaults",
|
|
5970
7060
|
name: "Backfill planner.budget defaults in config.yaml",
|
|
5971
7061
|
isApplied: (paths) => {
|
|
5972
|
-
if (!
|
|
7062
|
+
if (!fs35.existsSync(paths.configFile)) return true;
|
|
5973
7063
|
try {
|
|
5974
|
-
const raw =
|
|
7064
|
+
const raw = fs35.readFileSync(paths.configFile, "utf8");
|
|
5975
7065
|
if (!/^\s*planner:/m.test(raw)) return true;
|
|
5976
7066
|
if (/^\s*budget:/m.test(raw)) return true;
|
|
5977
7067
|
const cfg = loadConfig(paths.configFile);
|
|
@@ -5990,9 +7080,9 @@ var CONFIG_NORMALIZE_MIGRATION = {
|
|
|
5990
7080
|
id: "config-normalize",
|
|
5991
7081
|
name: "Normalise .squad/config.yaml formatting",
|
|
5992
7082
|
isApplied: (paths) => {
|
|
5993
|
-
if (!
|
|
7083
|
+
if (!fs35.existsSync(paths.configFile)) return true;
|
|
5994
7084
|
try {
|
|
5995
|
-
const raw =
|
|
7085
|
+
const raw = fs35.readFileSync(paths.configFile, "utf8");
|
|
5996
7086
|
const cfg = loadConfig(paths.configFile);
|
|
5997
7087
|
return raw.trim() === serializeConfig(cfg).trim();
|
|
5998
7088
|
} catch {
|
|
@@ -6001,7 +7091,7 @@ var CONFIG_NORMALIZE_MIGRATION = {
|
|
|
6001
7091
|
},
|
|
6002
7092
|
describe: () => "rewrite config.yaml with canonical YAML formatting (drops comments)",
|
|
6003
7093
|
apply: (paths) => {
|
|
6004
|
-
if (!
|
|
7094
|
+
if (!fs35.existsSync(paths.configFile)) return;
|
|
6005
7095
|
const cfg = loadConfig(paths.configFile);
|
|
6006
7096
|
saveConfig(paths.configFile, cfg);
|
|
6007
7097
|
}
|
|
@@ -6062,8 +7152,8 @@ async function runMigrate(opts) {
|
|
|
6062
7152
|
await Promise.resolve(m.apply(paths));
|
|
6063
7153
|
spin.succeed(`${m.name} \u2014 applied`);
|
|
6064
7154
|
applied++;
|
|
6065
|
-
} catch (
|
|
6066
|
-
spin.fail(`${m.name} \u2014 failed: ${
|
|
7155
|
+
} catch (err2) {
|
|
7156
|
+
spin.fail(`${m.name} \u2014 failed: ${err2.message}`);
|
|
6067
7157
|
process.exit(1);
|
|
6068
7158
|
}
|
|
6069
7159
|
}
|
|
@@ -6082,8 +7172,8 @@ import process3 from "process";
|
|
|
6082
7172
|
import { confirm as confirm8 } from "@inquirer/prompts";
|
|
6083
7173
|
|
|
6084
7174
|
// src/core/package-info.ts
|
|
6085
|
-
import
|
|
6086
|
-
import
|
|
7175
|
+
import fs36 from "fs";
|
|
7176
|
+
import path28 from "path";
|
|
6087
7177
|
function changelogBlobUrl(repositoryUrl, version) {
|
|
6088
7178
|
let base = repositoryUrl;
|
|
6089
7179
|
if (!base) base = "https://github.com/AzmSquad/squad-kit";
|
|
@@ -6094,9 +7184,9 @@ function changelogBlobUrl(repositoryUrl, version) {
|
|
|
6094
7184
|
}
|
|
6095
7185
|
function readInstalledPackage() {
|
|
6096
7186
|
const root = packageRoot();
|
|
6097
|
-
const pkgFile =
|
|
6098
|
-
const pkg = JSON.parse(
|
|
6099
|
-
const isDevInstall =
|
|
7187
|
+
const pkgFile = path28.join(root, "package.json");
|
|
7188
|
+
const pkg = JSON.parse(fs36.readFileSync(pkgFile, "utf8"));
|
|
7189
|
+
const isDevInstall = fs36.existsSync(path28.join(root, "src"));
|
|
6100
7190
|
return {
|
|
6101
7191
|
name: pkg.name,
|
|
6102
7192
|
version: pkg.version,
|
|
@@ -6136,7 +7226,7 @@ function isNewer(a, b) {
|
|
|
6136
7226
|
}
|
|
6137
7227
|
|
|
6138
7228
|
// src/core/package-manager.ts
|
|
6139
|
-
import
|
|
7229
|
+
import path29 from "path";
|
|
6140
7230
|
import process2 from "process";
|
|
6141
7231
|
function detectPackageManager(installRoot) {
|
|
6142
7232
|
const ua = process2.env.npm_config_user_agent;
|
|
@@ -6147,11 +7237,11 @@ function detectPackageManager(installRoot) {
|
|
|
6147
7237
|
if (ua.startsWith("npm/")) return { pm: "npm", reason: "npm_config_user_agent" };
|
|
6148
7238
|
}
|
|
6149
7239
|
const root = installRoot.toLowerCase();
|
|
6150
|
-
if (root.includes(`${
|
|
7240
|
+
if (root.includes(`${path29.sep}pnpm${path29.sep}`) || root.includes(`${path29.sep}.pnpm${path29.sep}`))
|
|
6151
7241
|
return { pm: "pnpm", reason: `install path contains pnpm` };
|
|
6152
|
-
if (root.includes(`${
|
|
7242
|
+
if (root.includes(`${path29.sep}.yarn${path29.sep}`) || root.includes(`${path29.sep}yarn${path29.sep}`))
|
|
6153
7243
|
return { pm: "yarn", reason: `install path contains yarn` };
|
|
6154
|
-
if (root.includes(`${
|
|
7244
|
+
if (root.includes(`${path29.sep}.bun${path29.sep}`) || root.includes(`${path29.sep}bun${path29.sep}`))
|
|
6155
7245
|
return { pm: "bun", reason: `install path contains bun` };
|
|
6156
7246
|
return { pm: "npm", reason: "default fallback" };
|
|
6157
7247
|
}
|
|
@@ -6185,9 +7275,9 @@ async function runUpgrade(opts) {
|
|
|
6185
7275
|
try {
|
|
6186
7276
|
const info2 = await fetchLatest(installed.name);
|
|
6187
7277
|
latest = info2.latest;
|
|
6188
|
-
} catch (
|
|
7278
|
+
} catch (err2) {
|
|
6189
7279
|
failure(
|
|
6190
|
-
`Could not reach the npm registry: ${
|
|
7280
|
+
`Could not reach the npm registry: ${err2.message}. Check your network, then run \`pnpm add -g squad-kit@latest\` (or \`npm i -g squad-kit@latest\`) manually.`
|
|
6191
7281
|
);
|
|
6192
7282
|
process3.exit(1);
|
|
6193
7283
|
}
|
|
@@ -6247,7 +7337,7 @@ function spawnInherit(cmd, args) {
|
|
|
6247
7337
|
return new Promise((resolve2, reject) => {
|
|
6248
7338
|
const child = spawn2(cmd, args, { stdio: "inherit", shell: process3.platform === "win32" });
|
|
6249
7339
|
child.on("exit", (code) => resolve2(code ?? 0));
|
|
6250
|
-
child.on("error", (
|
|
7340
|
+
child.on("error", (err2) => reject(err2));
|
|
6251
7341
|
});
|
|
6252
7342
|
}
|
|
6253
7343
|
|
|
@@ -6264,25 +7354,25 @@ import { cors } from "hono/cors";
|
|
|
6264
7354
|
import { secureHeaders } from "hono/secure-headers";
|
|
6265
7355
|
|
|
6266
7356
|
// src/console/api/stories.ts
|
|
6267
|
-
import
|
|
6268
|
-
import { z } from "zod";
|
|
7357
|
+
import fs37 from "fs";
|
|
7358
|
+
import { z as z5 } from "zod";
|
|
6269
7359
|
|
|
6270
7360
|
// src/utils/path-safety.ts
|
|
6271
|
-
import
|
|
7361
|
+
import path30 from "path";
|
|
6272
7362
|
function isInside(abs, parent) {
|
|
6273
|
-
const a =
|
|
6274
|
-
const p =
|
|
7363
|
+
const a = path30.resolve(abs) + path30.sep;
|
|
7364
|
+
const p = path30.resolve(parent) + path30.sep;
|
|
6275
7365
|
return a.startsWith(p);
|
|
6276
7366
|
}
|
|
6277
7367
|
|
|
6278
7368
|
// src/console/api/stories.ts
|
|
6279
|
-
var CreateBody =
|
|
6280
|
-
feature:
|
|
6281
|
-
title:
|
|
6282
|
-
trackerId:
|
|
7369
|
+
var CreateBody = z5.object({
|
|
7370
|
+
feature: z5.string().min(1).max(80),
|
|
7371
|
+
title: z5.string().max(200).optional(),
|
|
7372
|
+
trackerId: z5.string().max(80).optional()
|
|
6283
7373
|
});
|
|
6284
|
-
var PatchBody =
|
|
6285
|
-
intakeContent:
|
|
7374
|
+
var PatchBody = z5.object({
|
|
7375
|
+
intakeContent: z5.string().min(1).max(1e6)
|
|
6286
7376
|
});
|
|
6287
7377
|
function safeFeatureParam(feature) {
|
|
6288
7378
|
return feature.length > 0 && !feature.includes("/") && !feature.includes("..") && slugify(feature) === feature;
|
|
@@ -6311,7 +7401,7 @@ function mountStoriesApi(app, opts) {
|
|
|
6311
7401
|
if (!isInside(hit.intakePath, opts.paths.storiesDir)) return c.json({ error: "forbidden" }, 403);
|
|
6312
7402
|
let intakeContent = "";
|
|
6313
7403
|
try {
|
|
6314
|
-
intakeContent =
|
|
7404
|
+
intakeContent = fs37.readFileSync(hit.intakePath, "utf8");
|
|
6315
7405
|
} catch {
|
|
6316
7406
|
}
|
|
6317
7407
|
return c.json({
|
|
@@ -6355,7 +7445,7 @@ function mountStoriesApi(app, opts) {
|
|
|
6355
7445
|
const hit = stories.find((s) => s.id === id);
|
|
6356
7446
|
if (!hit) return c.json({ error: "not_found" }, 404);
|
|
6357
7447
|
if (!isInside(hit.intakePath, opts.paths.storiesDir)) return c.json({ error: "forbidden" }, 403);
|
|
6358
|
-
|
|
7448
|
+
fs37.writeFileSync(hit.intakePath, body.data.intakeContent, "utf8");
|
|
6359
7449
|
return c.json({ ok: true });
|
|
6360
7450
|
});
|
|
6361
7451
|
app.delete("/api/stories/:feature/:id", (c) => {
|
|
@@ -6372,14 +7462,14 @@ function mountStoriesApi(app, opts) {
|
|
|
6372
7462
|
}
|
|
6373
7463
|
|
|
6374
7464
|
// src/console/api/plans.ts
|
|
6375
|
-
import
|
|
6376
|
-
import
|
|
6377
|
-
import { z as
|
|
7465
|
+
import fs38 from "fs";
|
|
7466
|
+
import path31 from "path";
|
|
7467
|
+
import { z as z6 } from "zod";
|
|
6378
7468
|
import { diffLines } from "diff";
|
|
6379
|
-
var DiffQuery =
|
|
6380
|
-
feature:
|
|
6381
|
-
a:
|
|
6382
|
-
b:
|
|
7469
|
+
var DiffQuery = z6.object({
|
|
7470
|
+
feature: z6.string().min(1).max(80),
|
|
7471
|
+
a: z6.string().min(1).max(200),
|
|
7472
|
+
b: z6.string().min(1).max(200)
|
|
6383
7473
|
});
|
|
6384
7474
|
function safeFeatureParam2(feature) {
|
|
6385
7475
|
return feature.length > 0 && !feature.includes("/") && !feature.includes("..") && slugify(feature) === feature;
|
|
@@ -6388,11 +7478,11 @@ function mountPlansApi(app, opts) {
|
|
|
6388
7478
|
app.get("/api/plans", (c) => {
|
|
6389
7479
|
const out = [];
|
|
6390
7480
|
for (const feature of listPlanFeatureNames(opts.paths)) {
|
|
6391
|
-
const featureDir =
|
|
6392
|
-
if (!
|
|
6393
|
-
for (const entry of
|
|
7481
|
+
const featureDir = path31.join(opts.paths.plansDir, feature);
|
|
7482
|
+
if (!fs38.existsSync(featureDir)) continue;
|
|
7483
|
+
for (const entry of fs38.readdirSync(featureDir)) {
|
|
6394
7484
|
if (!entry.endsWith(".md") || entry === "00-index.md") continue;
|
|
6395
|
-
const abs =
|
|
7485
|
+
const abs = path31.join(featureDir, entry);
|
|
6396
7486
|
out.push({ feature, planFile: entry, metadata: readPlanMetadata(abs) });
|
|
6397
7487
|
}
|
|
6398
7488
|
}
|
|
@@ -6401,10 +7491,10 @@ function mountPlansApi(app, opts) {
|
|
|
6401
7491
|
app.get("/api/plans/:feature/:planFile", (c) => {
|
|
6402
7492
|
const { feature, planFile } = c.req.param();
|
|
6403
7493
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
6404
|
-
const abs =
|
|
7494
|
+
const abs = path31.resolve(path31.join(opts.paths.plansDir, feature, planFile));
|
|
6405
7495
|
if (!isInside(abs, opts.paths.plansDir)) return c.json({ error: "forbidden" }, 403);
|
|
6406
|
-
if (!
|
|
6407
|
-
const content =
|
|
7496
|
+
if (!fs38.existsSync(abs)) return c.json({ error: "not_found" }, 404);
|
|
7497
|
+
const content = fs38.readFileSync(abs, "utf8");
|
|
6408
7498
|
return c.json({
|
|
6409
7499
|
feature,
|
|
6410
7500
|
planFile,
|
|
@@ -6422,15 +7512,15 @@ function mountPlansApi(app, opts) {
|
|
|
6422
7512
|
if (!parse.success) return c.json({ error: "invalid_query", issues: parse.error.issues }, 400);
|
|
6423
7513
|
const { feature, a, b } = parse.data;
|
|
6424
7514
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
6425
|
-
const featureDir =
|
|
6426
|
-
const aAbs =
|
|
6427
|
-
const bAbs =
|
|
7515
|
+
const featureDir = path31.join(opts.paths.plansDir, feature);
|
|
7516
|
+
const aAbs = path31.join(featureDir, a);
|
|
7517
|
+
const bAbs = path31.join(featureDir, b);
|
|
6428
7518
|
if (!isInside(aAbs, opts.paths.plansDir) || !isInside(bAbs, opts.paths.plansDir)) {
|
|
6429
7519
|
return c.json({ error: "forbidden" }, 403);
|
|
6430
7520
|
}
|
|
6431
|
-
if (!
|
|
6432
|
-
const aText =
|
|
6433
|
-
const bText =
|
|
7521
|
+
if (!fs38.existsSync(aAbs) || !fs38.existsSync(bAbs)) return c.json({ error: "not_found" }, 404);
|
|
7522
|
+
const aText = fs38.readFileSync(aAbs, "utf8");
|
|
7523
|
+
const bText = fs38.readFileSync(bAbs, "utf8");
|
|
6434
7524
|
const changes = diffLines(aText, bText);
|
|
6435
7525
|
return c.json({ feature, a, b, changes });
|
|
6436
7526
|
});
|
|
@@ -6438,23 +7528,23 @@ function mountPlansApi(app, opts) {
|
|
|
6438
7528
|
const trash = c.req.query("trash") === "1";
|
|
6439
7529
|
const { feature, planFile } = c.req.param();
|
|
6440
7530
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
6441
|
-
const abs =
|
|
7531
|
+
const abs = path31.join(opts.paths.plansDir, feature, planFile);
|
|
6442
7532
|
if (!isInside(abs, opts.paths.plansDir)) return c.json({ error: "forbidden" }, 403);
|
|
6443
|
-
if (!
|
|
7533
|
+
if (!fs38.existsSync(abs)) return c.json({ error: "not_found" }, 404);
|
|
6444
7534
|
if (trash) {
|
|
6445
7535
|
const result = trashOrDelete([abs], opts.paths.trashDir, true);
|
|
6446
|
-
removeOverviewRowForPlanFile(
|
|
7536
|
+
removeOverviewRowForPlanFile(path31.join(opts.paths.plansDir, feature), planFile);
|
|
6447
7537
|
return c.json({ ok: true, trashed: result.trashed });
|
|
6448
7538
|
}
|
|
6449
|
-
|
|
6450
|
-
removeOverviewRowForPlanFile(
|
|
7539
|
+
fs38.rmSync(abs);
|
|
7540
|
+
removeOverviewRowForPlanFile(path31.join(opts.paths.plansDir, feature), planFile);
|
|
6451
7541
|
return c.json({ ok: true });
|
|
6452
7542
|
});
|
|
6453
7543
|
}
|
|
6454
7544
|
|
|
6455
7545
|
// src/console/api/config.ts
|
|
6456
|
-
import { z as
|
|
6457
|
-
var ConfigBody =
|
|
7546
|
+
import { z as z7 } from "zod";
|
|
7547
|
+
var ConfigBody = z7.object({}).passthrough();
|
|
6458
7548
|
function mountConfigApi(app, opts) {
|
|
6459
7549
|
app.get("/api/config", (c) => {
|
|
6460
7550
|
const config2 = loadConfig(opts.paths.configFile);
|
|
@@ -6466,8 +7556,8 @@ function mountConfigApi(app, opts) {
|
|
|
6466
7556
|
try {
|
|
6467
7557
|
saveConfig(opts.paths.configFile, body.data);
|
|
6468
7558
|
return c.json({ ok: true });
|
|
6469
|
-
} catch (
|
|
6470
|
-
return c.json({ error: "invalid_config", detail:
|
|
7559
|
+
} catch (err2) {
|
|
7560
|
+
return c.json({ error: "invalid_config", detail: err2.message }, 400);
|
|
6471
7561
|
}
|
|
6472
7562
|
});
|
|
6473
7563
|
}
|
|
@@ -6494,14 +7584,14 @@ function mountMetaApi(app, opts) {
|
|
|
6494
7584
|
|
|
6495
7585
|
// src/console/api/runs.ts
|
|
6496
7586
|
import { streamSSE } from "hono/streaming";
|
|
6497
|
-
import { z as
|
|
6498
|
-
import
|
|
6499
|
-
import
|
|
7587
|
+
import { z as z8 } from "zod";
|
|
7588
|
+
import path32 from "path";
|
|
7589
|
+
import fs39 from "fs";
|
|
6500
7590
|
var active = /* @__PURE__ */ new Map();
|
|
6501
7591
|
var EVENT_BUF_CAP = 512;
|
|
6502
|
-
var StartBody =
|
|
6503
|
-
feature:
|
|
6504
|
-
storyId:
|
|
7592
|
+
var StartBody = z8.object({
|
|
7593
|
+
feature: z8.string().min(1),
|
|
7594
|
+
storyId: z8.string().min(1)
|
|
6505
7595
|
});
|
|
6506
7596
|
function emptyStats(durationMs) {
|
|
6507
7597
|
return {
|
|
@@ -6557,11 +7647,36 @@ function mountRunsApi(app, opts) {
|
|
|
6557
7647
|
const done = (async () => {
|
|
6558
7648
|
const cfgFresh = loadConfig(opts.paths.configFile);
|
|
6559
7649
|
const planner = cfgFresh.planner;
|
|
6560
|
-
const
|
|
6561
|
-
const model =
|
|
7650
|
+
const modelId = modelFor(planner.provider, "plan", planner.modelOverride);
|
|
7651
|
+
const { model } = resolveModel(planner.provider, modelId, apiKey);
|
|
6562
7652
|
const budget = new Budget(planner.budget);
|
|
6563
7653
|
const cacheEnabled = planner.cache?.enabled ?? true;
|
|
6564
|
-
const
|
|
7654
|
+
const scoutEnabled = planner.stages?.scout?.enabled !== false;
|
|
7655
|
+
const validationEnabled = planner.validation?.enabled !== false;
|
|
7656
|
+
const strictValidation = planner.validation?.strict === true;
|
|
7657
|
+
const maxScoutFiles = planner.stages?.scout?.maxFiles ?? 12;
|
|
7658
|
+
let scoutModelId = "";
|
|
7659
|
+
let scoutModelHandle;
|
|
7660
|
+
let scoutSystemPrompt;
|
|
7661
|
+
if (scoutEnabled) {
|
|
7662
|
+
scoutModelId = modelFor(
|
|
7663
|
+
planner.provider,
|
|
7664
|
+
"scout",
|
|
7665
|
+
planner.modelOverride,
|
|
7666
|
+
planner.stages?.scout?.modelOverride
|
|
7667
|
+
);
|
|
7668
|
+
scoutModelHandle = resolveModel(planner.provider, scoutModelId, apiKey).model;
|
|
7669
|
+
}
|
|
7670
|
+
const repoMap = buildRepoMap(opts.paths.root, { format: "tree" });
|
|
7671
|
+
if (scoutEnabled) {
|
|
7672
|
+
scoutSystemPrompt = composeScoutSystemPrompt({
|
|
7673
|
+
projectRoots: cfgFresh.project.projectRoots ?? ["."],
|
|
7674
|
+
primaryLanguage: cfgFresh.project.primaryLanguage ?? "",
|
|
7675
|
+
trackerType: cfgFresh.tracker.type,
|
|
7676
|
+
repoMap,
|
|
7677
|
+
plansIndex: buildPlansIndex(opts.paths)
|
|
7678
|
+
});
|
|
7679
|
+
}
|
|
6565
7680
|
const systemPrompt = composeSystemPrompt({
|
|
6566
7681
|
projectRoots: cfgFresh.project.projectRoots ?? ["."],
|
|
6567
7682
|
primaryLanguage: cfgFresh.project.primaryLanguage ?? "",
|
|
@@ -6569,15 +7684,15 @@ function mountRunsApi(app, opts) {
|
|
|
6569
7684
|
repoMap
|
|
6570
7685
|
});
|
|
6571
7686
|
const userPrompt = composeUserPrompt({
|
|
6572
|
-
intakeContent:
|
|
7687
|
+
intakeContent: fs39.readFileSync(story.intakePath, "utf8")
|
|
6573
7688
|
});
|
|
6574
7689
|
let result;
|
|
6575
7690
|
try {
|
|
6576
7691
|
result = await runPlanner({
|
|
6577
7692
|
root: opts.paths.root,
|
|
6578
|
-
provider,
|
|
6579
7693
|
model,
|
|
6580
|
-
|
|
7694
|
+
provider: planner.provider,
|
|
7695
|
+
modelId,
|
|
6581
7696
|
systemPrompt,
|
|
6582
7697
|
userPrompt,
|
|
6583
7698
|
budget,
|
|
@@ -6585,14 +7700,26 @@ function mountRunsApi(app, opts) {
|
|
|
6585
7700
|
cacheEnabled,
|
|
6586
7701
|
events: bus,
|
|
6587
7702
|
runId,
|
|
6588
|
-
abort: controller.signal
|
|
7703
|
+
abort: controller.signal,
|
|
7704
|
+
stages: {
|
|
7705
|
+
scout: {
|
|
7706
|
+
enabled: scoutEnabled,
|
|
7707
|
+
model: scoutModelHandle,
|
|
7708
|
+
modelId: scoutModelId,
|
|
7709
|
+
maxFiles: maxScoutFiles,
|
|
7710
|
+
maxOutputTokens: planner.stages?.scout?.maxOutputTokens ?? 2048
|
|
7711
|
+
}
|
|
7712
|
+
},
|
|
7713
|
+
validation: { enabled: validationEnabled, strict: strictValidation },
|
|
7714
|
+
toolsEnabled: planner.tools,
|
|
7715
|
+
scoutSystemPrompt
|
|
6589
7716
|
});
|
|
6590
|
-
} catch (
|
|
7717
|
+
} catch (err2) {
|
|
6591
7718
|
const durationMs = Date.now() - startedAt;
|
|
6592
7719
|
const last = eventBuffer[eventBuffer.length - 1];
|
|
6593
7720
|
const isRateLimitAbort = last?.kind === "rate_limit" && last.phase === "aborted";
|
|
6594
7721
|
if (!isRateLimitAbort) {
|
|
6595
|
-
bus.emit({ kind: "error", runId, message:
|
|
7722
|
+
bus.emit({ kind: "error", runId, message: err2.message });
|
|
6596
7723
|
}
|
|
6597
7724
|
bus.emit({
|
|
6598
7725
|
kind: "done",
|
|
@@ -6605,20 +7732,25 @@ function mountRunsApi(app, opts) {
|
|
|
6605
7732
|
});
|
|
6606
7733
|
return;
|
|
6607
7734
|
}
|
|
6608
|
-
const
|
|
7735
|
+
const issueCount = result.validation?.issues.length ?? 0;
|
|
7736
|
+
const issuesByKind = summariseIssuesByKind(result.validation?.issues ?? []);
|
|
7737
|
+
const validationBlocks = strictValidation && validationEnabled && issueCount > 0;
|
|
7738
|
+
const runSuccess = result.finishedNormally && !result.timedOut && !result.userCancelled && !validationBlocks;
|
|
6609
7739
|
let planFile = null;
|
|
6610
7740
|
if (result.planText.trim()) {
|
|
6611
7741
|
const snap = budget.snapshot();
|
|
6612
7742
|
const header = buildMetadataHeader({
|
|
6613
7743
|
provider: planner.provider,
|
|
6614
|
-
model,
|
|
7744
|
+
model: modelId,
|
|
6615
7745
|
reads: snap.reads,
|
|
6616
7746
|
bytes: snap.bytes,
|
|
6617
7747
|
inputTokens: snap.usage.inputTokens,
|
|
6618
7748
|
outputTokens: snap.usage.outputTokens,
|
|
6619
7749
|
durationMs: Date.now() - startedAt,
|
|
6620
|
-
|
|
6621
|
-
|
|
7750
|
+
planStatus: runSuccess ? void 0 : "partial",
|
|
7751
|
+
scoutEnabled,
|
|
7752
|
+
validationEnabled,
|
|
7753
|
+
validationIssueCount: issueCount
|
|
6622
7754
|
});
|
|
6623
7755
|
const out = writePlanFile({
|
|
6624
7756
|
paths: opts.paths,
|
|
@@ -6628,19 +7760,19 @@ function mountRunsApi(app, opts) {
|
|
|
6628
7760
|
metadataHeader: header,
|
|
6629
7761
|
partial: !runSuccess
|
|
6630
7762
|
});
|
|
6631
|
-
planFile =
|
|
7763
|
+
planFile = path32.relative(opts.paths.root, out.planFile);
|
|
6632
7764
|
}
|
|
6633
7765
|
try {
|
|
6634
7766
|
await writeLastRun(opts.paths, {
|
|
6635
7767
|
stats: result.stats,
|
|
6636
7768
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6637
7769
|
provider: planner.provider,
|
|
6638
|
-
model
|
|
7770
|
+
model: modelId
|
|
6639
7771
|
});
|
|
6640
7772
|
await appendRun(opts.paths, {
|
|
6641
7773
|
runId,
|
|
6642
7774
|
provider: planner.provider,
|
|
6643
|
-
model,
|
|
7775
|
+
model: modelId,
|
|
6644
7776
|
feature: story.feature,
|
|
6645
7777
|
storyId: story.id,
|
|
6646
7778
|
startedAt: new Date(startedAt).toISOString(),
|
|
@@ -6650,7 +7782,19 @@ function mountRunsApi(app, opts) {
|
|
|
6650
7782
|
planFile,
|
|
6651
7783
|
stats: result.stats,
|
|
6652
7784
|
cacheEnabled,
|
|
6653
|
-
durationMs: Date.now() - startedAt
|
|
7785
|
+
durationMs: Date.now() - startedAt,
|
|
7786
|
+
scout: {
|
|
7787
|
+
enabled: scoutEnabled,
|
|
7788
|
+
selectedCount: result.scout?.selected.length,
|
|
7789
|
+
tokensUsed: result.scout?.tokensUsed,
|
|
7790
|
+
durationMs: result.scout?.durationMs
|
|
7791
|
+
},
|
|
7792
|
+
validation: {
|
|
7793
|
+
enabled: validationEnabled,
|
|
7794
|
+
issuesCount: issueCount,
|
|
7795
|
+
issuesByKind,
|
|
7796
|
+
durationMs: result.validation?.durationMs
|
|
7797
|
+
}
|
|
6654
7798
|
});
|
|
6655
7799
|
} catch {
|
|
6656
7800
|
}
|
|
@@ -6723,7 +7867,7 @@ function mountRunsApi(app, opts) {
|
|
|
6723
7867
|
}
|
|
6724
7868
|
|
|
6725
7869
|
// src/console/api/secrets.ts
|
|
6726
|
-
import { z as
|
|
7870
|
+
import { z as z9 } from "zod";
|
|
6727
7871
|
|
|
6728
7872
|
// src/core/mask-token.ts
|
|
6729
7873
|
function maskToken(t) {
|
|
@@ -6761,26 +7905,26 @@ function mountSecretsApi(app, opts) {
|
|
|
6761
7905
|
}
|
|
6762
7906
|
});
|
|
6763
7907
|
});
|
|
6764
|
-
const PutBody =
|
|
6765
|
-
planner:
|
|
6766
|
-
anthropic:
|
|
6767
|
-
openai:
|
|
6768
|
-
google:
|
|
7908
|
+
const PutBody = z9.object({
|
|
7909
|
+
planner: z9.object({
|
|
7910
|
+
anthropic: z9.string().optional(),
|
|
7911
|
+
openai: z9.string().optional(),
|
|
7912
|
+
google: z9.string().optional()
|
|
6769
7913
|
}).optional(),
|
|
6770
|
-
tracker:
|
|
6771
|
-
jira:
|
|
6772
|
-
host:
|
|
6773
|
-
email:
|
|
6774
|
-
token:
|
|
7914
|
+
tracker: z9.object({
|
|
7915
|
+
jira: z9.object({
|
|
7916
|
+
host: z9.string().optional(),
|
|
7917
|
+
email: z9.string().optional(),
|
|
7918
|
+
token: z9.string().optional()
|
|
6775
7919
|
}).optional(),
|
|
6776
|
-
azure:
|
|
6777
|
-
organization:
|
|
6778
|
-
project:
|
|
6779
|
-
pat:
|
|
7920
|
+
azure: z9.object({
|
|
7921
|
+
organization: z9.string().optional(),
|
|
7922
|
+
project: z9.string().optional(),
|
|
7923
|
+
pat: z9.string().optional()
|
|
6780
7924
|
}).optional(),
|
|
6781
|
-
github:
|
|
6782
|
-
host:
|
|
6783
|
-
pat:
|
|
7925
|
+
github: z9.object({
|
|
7926
|
+
host: z9.string().optional(),
|
|
7927
|
+
pat: z9.string().optional()
|
|
6784
7928
|
}).optional()
|
|
6785
7929
|
}).optional()
|
|
6786
7930
|
});
|
|
@@ -6820,12 +7964,12 @@ function mountSecretsApi(app, opts) {
|
|
|
6820
7964
|
}
|
|
6821
7965
|
|
|
6822
7966
|
// src/console/api/tracker.ts
|
|
6823
|
-
import { z as
|
|
6824
|
-
var SearchQuery =
|
|
6825
|
-
var ImportBody =
|
|
6826
|
-
issueId:
|
|
6827
|
-
feature:
|
|
6828
|
-
withAttachments:
|
|
7967
|
+
import { z as z10 } from "zod";
|
|
7968
|
+
var SearchQuery = z10.object({ q: z10.string().max(500) });
|
|
7969
|
+
var ImportBody = z10.object({
|
|
7970
|
+
issueId: z10.string().min(1),
|
|
7971
|
+
feature: z10.string().min(1).max(80),
|
|
7972
|
+
withAttachments: z10.boolean().optional()
|
|
6829
7973
|
});
|
|
6830
7974
|
function mountTrackerApi(app, opts) {
|
|
6831
7975
|
app.get("/api/tracker/search", async (c) => {
|
|
@@ -6833,18 +7977,18 @@ function mountTrackerApi(app, opts) {
|
|
|
6833
7977
|
if (!q.success) return c.json({ error: "invalid_query" }, 400);
|
|
6834
7978
|
const cfg = loadConfig(opts.paths.configFile);
|
|
6835
7979
|
const secrets = overlayTrackerEnv(loadSecrets(opts.paths.secretsFile));
|
|
6836
|
-
const { client, error
|
|
7980
|
+
const { client, error } = clientFor(cfg, secrets);
|
|
6837
7981
|
if (!client) {
|
|
6838
|
-
return c.json({ error:
|
|
7982
|
+
return c.json({ error: error?.kind ?? "unsupported", detail: error?.detail ?? "" }, 400);
|
|
6839
7983
|
}
|
|
6840
7984
|
try {
|
|
6841
7985
|
const results = await client.searchIssues(q.data.q);
|
|
6842
7986
|
return c.json({ ok: true, results });
|
|
6843
|
-
} catch (
|
|
6844
|
-
if (
|
|
6845
|
-
return c.json({ ok: false, kind:
|
|
7987
|
+
} catch (err2) {
|
|
7988
|
+
if (err2 instanceof TrackerError) {
|
|
7989
|
+
return c.json({ ok: false, kind: err2.kind, status: err2.statusCode, detail: err2.message }, 200);
|
|
6846
7990
|
}
|
|
6847
|
-
return c.json({ ok: false, detail:
|
|
7991
|
+
return c.json({ ok: false, detail: err2.message }, 200);
|
|
6848
7992
|
}
|
|
6849
7993
|
});
|
|
6850
7994
|
app.post("/api/tracker/import", async (c) => {
|
|
@@ -6852,9 +7996,9 @@ function mountTrackerApi(app, opts) {
|
|
|
6852
7996
|
if (!body.success) return c.json({ error: "invalid_body", issues: body.error.issues }, 400);
|
|
6853
7997
|
const cfg = loadConfig(opts.paths.configFile);
|
|
6854
7998
|
const secrets = overlayTrackerEnv(loadSecrets(opts.paths.secretsFile));
|
|
6855
|
-
const { client, error
|
|
7999
|
+
const { client, error } = clientFor(cfg, secrets);
|
|
6856
8000
|
if (!client) {
|
|
6857
|
-
return c.json({ error:
|
|
8001
|
+
return c.json({ error: error?.kind ?? "unsupported", detail: error?.detail ?? "" }, 400);
|
|
6858
8002
|
}
|
|
6859
8003
|
try {
|
|
6860
8004
|
const issue = await client.fetchIssue(body.data.issueId);
|
|
@@ -6867,9 +8011,9 @@ function mountTrackerApi(app, opts) {
|
|
|
6867
8011
|
config: cfg
|
|
6868
8012
|
});
|
|
6869
8013
|
return c.json(created, 201);
|
|
6870
|
-
} catch (
|
|
6871
|
-
if (
|
|
6872
|
-
return c.json({ error: "fetch_failed", detail:
|
|
8014
|
+
} catch (err2) {
|
|
8015
|
+
if (err2 instanceof TrackerError) return c.json({ error: err2.kind, detail: err2.message }, 502);
|
|
8016
|
+
return c.json({ error: "fetch_failed", detail: err2.message }, 500);
|
|
6873
8017
|
}
|
|
6874
8018
|
});
|
|
6875
8019
|
}
|
|
@@ -6966,17 +8110,17 @@ function countStories(stories) {
|
|
|
6966
8110
|
}
|
|
6967
8111
|
|
|
6968
8112
|
// src/console/api/projects.ts
|
|
6969
|
-
import
|
|
6970
|
-
import
|
|
8113
|
+
import fs40 from "fs/promises";
|
|
8114
|
+
import path33 from "path";
|
|
6971
8115
|
import os from "os";
|
|
6972
8116
|
var MAX = 10;
|
|
6973
8117
|
function recentFilePath() {
|
|
6974
|
-
return
|
|
8118
|
+
return path33.join(os.homedir(), ".squad", "recent-projects.json");
|
|
6975
8119
|
}
|
|
6976
8120
|
async function loadRecent() {
|
|
6977
8121
|
const RECENT_FILE = recentFilePath();
|
|
6978
8122
|
try {
|
|
6979
|
-
const raw = await
|
|
8123
|
+
const raw = await fs40.readFile(RECENT_FILE, "utf8");
|
|
6980
8124
|
const parsed = JSON.parse(raw);
|
|
6981
8125
|
return Array.isArray(parsed) ? parsed.slice(0, MAX) : [];
|
|
6982
8126
|
} catch {
|
|
@@ -6985,8 +8129,8 @@ async function loadRecent() {
|
|
|
6985
8129
|
}
|
|
6986
8130
|
async function saveRecent(list) {
|
|
6987
8131
|
const RECENT_FILE = recentFilePath();
|
|
6988
|
-
await
|
|
6989
|
-
await
|
|
8132
|
+
await fs40.mkdir(path33.dirname(RECENT_FILE), { recursive: true });
|
|
8133
|
+
await fs40.writeFile(RECENT_FILE, JSON.stringify(list.slice(0, MAX), null, 2) + "\n", "utf8");
|
|
6990
8134
|
}
|
|
6991
8135
|
function mountProjectsApi(app, opts) {
|
|
6992
8136
|
app.get("/api/projects/recent", async (c) => c.json(await loadRecent()));
|
|
@@ -7015,8 +8159,8 @@ function mountApi(app, opts) {
|
|
|
7015
8159
|
}
|
|
7016
8160
|
|
|
7017
8161
|
// src/console/static-assets.ts
|
|
7018
|
-
import
|
|
7019
|
-
import
|
|
8162
|
+
import fs41 from "fs";
|
|
8163
|
+
import path34 from "path";
|
|
7020
8164
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7021
8165
|
import { Readable } from "stream";
|
|
7022
8166
|
function pathnameForStatic(c) {
|
|
@@ -7046,18 +8190,18 @@ function mountStaticAssets(app, _opts) {
|
|
|
7046
8190
|
}
|
|
7047
8191
|
if (c.req.path.startsWith("/api/") || c.req.path.startsWith("/healthz")) return c.notFound();
|
|
7048
8192
|
if (!distDir) return c.text("UI not built. Run `pnpm -C console-ui build`.", 404);
|
|
7049
|
-
const indexHtml =
|
|
7050
|
-
if (!
|
|
7051
|
-
return c.html(
|
|
8193
|
+
const indexHtml = path34.join(distDir, "index.html");
|
|
8194
|
+
if (!fs41.existsSync(indexHtml)) return c.text("UI not built", 404);
|
|
8195
|
+
return c.html(fs41.readFileSync(indexHtml, "utf8"), 200, { "cache-control": "no-store" });
|
|
7052
8196
|
});
|
|
7053
8197
|
}
|
|
7054
8198
|
function assetResponse(c, distDir, pathname) {
|
|
7055
8199
|
if (!distDir) return c.text("UI not built", 404);
|
|
7056
8200
|
const rel = pathname.replace(/^\//, "");
|
|
7057
|
-
const abs =
|
|
8201
|
+
const abs = path34.normalize(path34.join(distDir, rel));
|
|
7058
8202
|
if (!isInside2(abs, distDir)) return c.text("forbidden", 403);
|
|
7059
|
-
if (!
|
|
7060
|
-
const body = Readable.toWeb(
|
|
8203
|
+
if (!fs41.existsSync(abs) || !fs41.statSync(abs).isFile()) return c.text("not found", 404);
|
|
8204
|
+
const body = Readable.toWeb(fs41.createReadStream(abs));
|
|
7061
8205
|
return new Response(body, {
|
|
7062
8206
|
headers: {
|
|
7063
8207
|
"content-type": contentTypeFor(abs),
|
|
@@ -7067,9 +8211,9 @@ function assetResponse(c, distDir, pathname) {
|
|
|
7067
8211
|
}
|
|
7068
8212
|
function serveFile(c, distDir, name, ct) {
|
|
7069
8213
|
if (!distDir) return c.text("UI not built", 404);
|
|
7070
|
-
const abs =
|
|
7071
|
-
if (!
|
|
7072
|
-
return new Response(
|
|
8214
|
+
const abs = path34.join(distDir, name);
|
|
8215
|
+
if (!fs41.existsSync(abs)) return c.text("not found", 404);
|
|
8216
|
+
return new Response(fs41.readFileSync(abs), {
|
|
7073
8217
|
headers: { "content-type": ct, "cache-control": "public, max-age=86400" }
|
|
7074
8218
|
});
|
|
7075
8219
|
}
|
|
@@ -7084,13 +8228,13 @@ function contentTypeFor(p) {
|
|
|
7084
8228
|
return "application/octet-stream";
|
|
7085
8229
|
}
|
|
7086
8230
|
function isInside2(abs, parent) {
|
|
7087
|
-
const a =
|
|
7088
|
-
const p =
|
|
8231
|
+
const a = path34.resolve(abs) + path34.sep;
|
|
8232
|
+
const p = path34.resolve(parent) + path34.sep;
|
|
7089
8233
|
return a.startsWith(p);
|
|
7090
8234
|
}
|
|
7091
8235
|
function resolveConsoleUiDist() {
|
|
7092
8236
|
const fromEnv = process.env.SQUAD_CONSOLE_UI_DIST;
|
|
7093
|
-
if (fromEnv &&
|
|
8237
|
+
if (fromEnv && fs41.existsSync(path34.join(fromEnv, "index.html"))) return fromEnv;
|
|
7094
8238
|
const candidates = [
|
|
7095
8239
|
fileURLToPath2(new URL("./console-ui/", import.meta.url)),
|
|
7096
8240
|
// dist/cli.js → dist/console-ui/
|
|
@@ -7102,7 +8246,7 @@ function resolveConsoleUiDist() {
|
|
|
7102
8246
|
// src/console/*.ts → project/console-ui/dist
|
|
7103
8247
|
];
|
|
7104
8248
|
for (const c of candidates) {
|
|
7105
|
-
if (
|
|
8249
|
+
if (fs41.existsSync(path34.join(c, "index.html"))) return c;
|
|
7106
8250
|
}
|
|
7107
8251
|
return null;
|
|
7108
8252
|
}
|
|
@@ -7178,13 +8322,13 @@ async function startConsoleServer(opts) {
|
|
|
7178
8322
|
return bindPort;
|
|
7179
8323
|
})();
|
|
7180
8324
|
const shutdown = (onClose) => {
|
|
7181
|
-
nodeServer.close((
|
|
7182
|
-
onClose(
|
|
8325
|
+
nodeServer.close((err2) => {
|
|
8326
|
+
onClose(err2);
|
|
7183
8327
|
});
|
|
7184
8328
|
};
|
|
7185
8329
|
const stop = () => {
|
|
7186
|
-
shutdown((
|
|
7187
|
-
if (
|
|
8330
|
+
shutdown((err2) => {
|
|
8331
|
+
if (err2) warning(`server close: ${err2.message}`);
|
|
7188
8332
|
resolveDone();
|
|
7189
8333
|
});
|
|
7190
8334
|
};
|
|
@@ -7194,9 +8338,9 @@ async function startConsoleServer(opts) {
|
|
|
7194
8338
|
port: actualPort,
|
|
7195
8339
|
done,
|
|
7196
8340
|
close: () => new Promise((resolve2, reject) => {
|
|
7197
|
-
shutdown((
|
|
7198
|
-
if (
|
|
7199
|
-
reject(
|
|
8341
|
+
shutdown((err2) => {
|
|
8342
|
+
if (err2) {
|
|
8343
|
+
reject(err2);
|
|
7200
8344
|
return;
|
|
7201
8345
|
}
|
|
7202
8346
|
resolve2();
|
|
@@ -7284,18 +8428,18 @@ async function runConsole(opts) {
|
|
|
7284
8428
|
}
|
|
7285
8429
|
|
|
7286
8430
|
// src/commands/rm/story.ts
|
|
7287
|
-
import
|
|
7288
|
-
import
|
|
8431
|
+
import fs43 from "fs";
|
|
8432
|
+
import path36 from "path";
|
|
7289
8433
|
import { confirm as confirm10 } from "@inquirer/prompts";
|
|
7290
8434
|
|
|
7291
8435
|
// src/commands/rm/feature.ts
|
|
7292
|
-
import
|
|
7293
|
-
import
|
|
8436
|
+
import fs42 from "fs";
|
|
8437
|
+
import path35 from "path";
|
|
7294
8438
|
import { confirm as confirm9 } from "@inquirer/prompts";
|
|
7295
8439
|
function countStoryDirs(storiesDir, feature) {
|
|
7296
|
-
const d =
|
|
7297
|
-
if (!
|
|
7298
|
-
return
|
|
8440
|
+
const d = path35.join(storiesDir, feature);
|
|
8441
|
+
if (!fs42.existsSync(d)) return 0;
|
|
8442
|
+
return fs42.readdirSync(d, { withFileTypes: true }).filter((x) => x.isDirectory()).length;
|
|
7299
8443
|
}
|
|
7300
8444
|
async function runRmFeature(featureSlug, opts = {}) {
|
|
7301
8445
|
const root = requireSquadRoot();
|
|
@@ -7329,17 +8473,17 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
7329
8473
|
feature = await pickFeatureInteractive(paths);
|
|
7330
8474
|
}
|
|
7331
8475
|
}
|
|
7332
|
-
const storyDir =
|
|
7333
|
-
const planDir =
|
|
8476
|
+
const storyDir = path35.join(paths.storiesDir, feature);
|
|
8477
|
+
const planDir = path35.join(paths.plansDir, feature);
|
|
7334
8478
|
const nStories = countStoryDirs(paths.storiesDir, feature);
|
|
7335
8479
|
const nPlans = listAllPlanListEntries(paths, feature).length;
|
|
7336
|
-
const hasOverview =
|
|
8480
|
+
const hasOverview = fs42.existsSync(path35.join(planDir, "00-overview.md")) ? 1 : 0;
|
|
7337
8481
|
const targets = [];
|
|
7338
|
-
if (
|
|
7339
|
-
targets.push({ label:
|
|
8482
|
+
if (fs42.existsSync(storyDir)) {
|
|
8483
|
+
targets.push({ label: path35.relative(root, storyDir), absPath: storyDir, kind: "dir" });
|
|
7340
8484
|
}
|
|
7341
|
-
if (
|
|
7342
|
-
targets.push({ label:
|
|
8485
|
+
if (fs42.existsSync(planDir)) {
|
|
8486
|
+
targets.push({ label: path35.relative(root, planDir), absPath: planDir, kind: "dir" });
|
|
7343
8487
|
}
|
|
7344
8488
|
divider("squad rm feature");
|
|
7345
8489
|
info(`Feature: ${feature}`);
|
|
@@ -7354,7 +8498,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
7354
8498
|
kv(t.kind, t.label, 6);
|
|
7355
8499
|
}
|
|
7356
8500
|
if (useTrash) {
|
|
7357
|
-
info(`Items will move to ${
|
|
8501
|
+
info(`Items will move to ${path35.relative(process.cwd(), paths.trashDir)}/<timestamp>/`);
|
|
7358
8502
|
} else if (!dryRun) {
|
|
7359
8503
|
warning("This is a permanent delete. Pass --trash to move to .squad/.trash/ instead.");
|
|
7360
8504
|
}
|
|
@@ -7387,7 +8531,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
7387
8531
|
useTrash
|
|
7388
8532
|
);
|
|
7389
8533
|
if (result.trashed) {
|
|
7390
|
-
success(`Feature removed. Items moved to ${
|
|
8534
|
+
success(`Feature removed. Items moved to ${path35.relative(root, result.trashed)}.`);
|
|
7391
8535
|
} else {
|
|
7392
8536
|
success("Feature removed (stories and plans directories).");
|
|
7393
8537
|
}
|
|
@@ -7428,11 +8572,11 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
7428
8572
|
}
|
|
7429
8573
|
}
|
|
7430
8574
|
const targets = [
|
|
7431
|
-
{ label:
|
|
8575
|
+
{ label: path36.relative(root, story.storyDir), absPath: story.storyDir, kind: "dir" }
|
|
7432
8576
|
];
|
|
7433
8577
|
if (story.planFile) {
|
|
7434
|
-
const planAbs =
|
|
7435
|
-
targets.push({ label:
|
|
8578
|
+
const planAbs = path36.join(paths.plansDir, story.feature, story.planFile);
|
|
8579
|
+
targets.push({ label: path36.relative(root, planAbs), absPath: planAbs, kind: "file" });
|
|
7436
8580
|
}
|
|
7437
8581
|
renderPreview("story", targets, {
|
|
7438
8582
|
dryRun,
|
|
@@ -7457,14 +8601,14 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
7457
8601
|
}
|
|
7458
8602
|
const result = deleteStoryRecord({ paths, story, trash: useTrash });
|
|
7459
8603
|
if (result.trashed) {
|
|
7460
|
-
success(`Removed (moved to ${
|
|
8604
|
+
success(`Removed (moved to ${path36.relative(root, result.trashed)}).`);
|
|
7461
8605
|
} else {
|
|
7462
8606
|
success("Removed story folder, plan file (if any), and overview row.");
|
|
7463
8607
|
}
|
|
7464
|
-
const featurePlanDir =
|
|
7465
|
-
const overviewPath =
|
|
7466
|
-
if (
|
|
7467
|
-
const body =
|
|
8608
|
+
const featurePlanDir = path36.join(paths.plansDir, story.feature);
|
|
8609
|
+
const overviewPath = path36.join(featurePlanDir, "00-overview.md");
|
|
8610
|
+
if (fs43.existsSync(overviewPath)) {
|
|
8611
|
+
const body = fs43.readFileSync(overviewPath, "utf8");
|
|
7468
8612
|
const onlyShell = countOverviewDataRows(body) === 0;
|
|
7469
8613
|
if (onlyShell && featureStoriesDirEmpty(paths, story.feature)) {
|
|
7470
8614
|
if (canPrompt) {
|
|
@@ -7484,7 +8628,7 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
7484
8628
|
}
|
|
7485
8629
|
|
|
7486
8630
|
// src/commands/rm/plan.ts
|
|
7487
|
-
import
|
|
8631
|
+
import path37 from "path";
|
|
7488
8632
|
import { confirm as confirm11 } from "@inquirer/prompts";
|
|
7489
8633
|
async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
7490
8634
|
const root = requireSquadRoot();
|
|
@@ -7520,7 +8664,7 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
7520
8664
|
}
|
|
7521
8665
|
const storyRows = listStories(paths, { feature: plan.feature });
|
|
7522
8666
|
const linked = storyRows.find((s) => s.planFile === plan.file);
|
|
7523
|
-
const targets = [{ label:
|
|
8667
|
+
const targets = [{ label: path37.relative(root, plan.abs), absPath: plan.abs, kind: "file" }];
|
|
7524
8668
|
renderPreview("plan", targets, {
|
|
7525
8669
|
dryRun,
|
|
7526
8670
|
trash: useTrash,
|
|
@@ -7543,8 +8687,8 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
7543
8687
|
}
|
|
7544
8688
|
}
|
|
7545
8689
|
trashOrDelete([plan.abs], paths.trashDir, useTrash);
|
|
7546
|
-
removeOverviewRowForPlanFile(
|
|
7547
|
-
const intakeHint = linked ?
|
|
8690
|
+
removeOverviewRowForPlanFile(path37.join(paths.plansDir, plan.feature), plan.file);
|
|
8691
|
+
const intakeHint = linked ? path37.relative(root, linked.intakePath) : `under .squad/stories/${plan.feature}/`;
|
|
7548
8692
|
success(`Plan removed. Run \`squad new-plan ${intakeHint}\` to regenerate.`);
|
|
7549
8693
|
}
|
|
7550
8694
|
|
|
@@ -7578,7 +8722,11 @@ program.command("new-story [feature-slug]").description("Scaffold a new story in
|
|
|
7578
8722
|
"--no-tracker",
|
|
7579
8723
|
"Skip tracker fetch AND drop the tracker-id requirement for this story"
|
|
7580
8724
|
).action(wrapArgs(runNewStory));
|
|
7581
|
-
program.command("new-plan [intake-path]").description("Generate a plan (via direct API when configured, otherwise copy-paste)").option("--no-clipboard", "Do not copy the composed prompt to clipboard (copy-paste mode only)").option("--feature <slug>", "Filter picker by feature slug").option("--all", "Include already-planned intakes in the picker", false).option("-y, --yes", "Skip confirmation prompts", false).option("--api", "Force direct planner API (fails if not configured)", false).option("--copy", "Force copy-paste mode even when planner API is configured", false).
|
|
8725
|
+
program.command("new-plan [intake-path]").description("Generate a plan (via direct API when configured, otherwise copy-paste)").option("--no-clipboard", "Do not copy the composed prompt to clipboard (copy-paste mode only)").option("--feature <slug>", "Filter picker by feature slug").option("--all", "Include already-planned intakes in the picker", false).option("-y, --yes", "Skip confirmation prompts", false).option("--api", "Force direct planner API (fails if not configured)", false).option("--copy", "Force copy-paste mode even when planner API is configured", false).option("--no-scout", "Disable the scout stage (draft only)", false).option("--scout-model <id>", "Override scout model id (cheap tier default from PLANNER_MODEL_MAP)").option(
|
|
8726
|
+
"--max-scout-files <n>",
|
|
8727
|
+
"Maximum files the scout can pre-select (default from config or 12)",
|
|
8728
|
+
(v) => parseInt(v, 10)
|
|
8729
|
+
).option("--no-validation", "Disable post-write validation", false).option("--strict-validation", "Write partial plan when validation finds issues", false).action(wrapArgs(runNewPlan));
|
|
7582
8730
|
program.command("status").description("Show squad-kit workspace status").action(wrap(runStatus));
|
|
7583
8731
|
program.command("doctor").description("Run a health check on the local .squad/ workspace and its external integrations").option("--fix", "Apply non-destructive repairs (gitignore, permissions, missing dirs)", false).option("--json", "Emit results as JSON to stdout (for scripting)", false).action(wrap(runDoctor));
|
|
7584
8732
|
program.command("migrate").description("Apply one-shot structural migrations to bring .squad/ to the latest version").option("--dry-run", "Show what would change without touching the filesystem", false).option("-y, --yes", "Skip the confirmation prompt", false).action(wrap(runMigrate));
|
|
@@ -7608,24 +8756,24 @@ config.command("remove-credential <section>").description('Delete credentials fo
|
|
|
7608
8756
|
(section, opts) => runConfigRemoveCredential(section, { yes: opts.yes })
|
|
7609
8757
|
)
|
|
7610
8758
|
);
|
|
7611
|
-
program.parseAsync(process.argv).catch((
|
|
7612
|
-
if (isSquadExit(
|
|
7613
|
-
if (
|
|
7614
|
-
process.exit(
|
|
8759
|
+
program.parseAsync(process.argv).catch((err2) => {
|
|
8760
|
+
if (isSquadExit(err2)) {
|
|
8761
|
+
if (err2.message.trim()) info(err2.message);
|
|
8762
|
+
process.exit(err2.exitCode);
|
|
7615
8763
|
}
|
|
7616
|
-
renderError(
|
|
8764
|
+
renderError(err2);
|
|
7617
8765
|
process.exit(1);
|
|
7618
8766
|
});
|
|
7619
8767
|
function wrap(fn) {
|
|
7620
8768
|
return async (opts) => {
|
|
7621
8769
|
try {
|
|
7622
8770
|
await fn(opts);
|
|
7623
|
-
} catch (
|
|
7624
|
-
if (isSquadExit(
|
|
7625
|
-
if (
|
|
7626
|
-
process.exit(
|
|
8771
|
+
} catch (err2) {
|
|
8772
|
+
if (isSquadExit(err2)) {
|
|
8773
|
+
if (err2.message.trim()) info(err2.message);
|
|
8774
|
+
process.exit(err2.exitCode);
|
|
7627
8775
|
}
|
|
7628
|
-
renderError(
|
|
8776
|
+
renderError(err2);
|
|
7629
8777
|
process.exit(1);
|
|
7630
8778
|
}
|
|
7631
8779
|
};
|
|
@@ -7634,12 +8782,12 @@ function wrapArgs(fn) {
|
|
|
7634
8782
|
return async (...args) => {
|
|
7635
8783
|
try {
|
|
7636
8784
|
await fn(...args);
|
|
7637
|
-
} catch (
|
|
7638
|
-
if (isSquadExit(
|
|
7639
|
-
if (
|
|
7640
|
-
process.exit(
|
|
8785
|
+
} catch (err2) {
|
|
8786
|
+
if (isSquadExit(err2)) {
|
|
8787
|
+
if (err2.message.trim()) info(err2.message);
|
|
8788
|
+
process.exit(err2.exitCode);
|
|
7641
8789
|
}
|
|
7642
|
-
renderError(
|
|
8790
|
+
renderError(err2);
|
|
7643
8791
|
process.exit(1);
|
|
7644
8792
|
}
|
|
7645
8793
|
};
|