squad-kit 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/dist/cli.js +2300 -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,357 @@ 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
|
+
const anthropicThinking = input5.provider === "anthropic" ? { anthropic: { thinking: { type: "enabled", budgetTokens: 1024 } } } : void 0;
|
|
4222
|
+
let attempt = 0;
|
|
4223
|
+
while (true) {
|
|
4224
|
+
try {
|
|
4225
|
+
const { object, usage, providerMetadata } = await generateObject({
|
|
4226
|
+
model: input5.model,
|
|
4227
|
+
system: input5.systemPrompt,
|
|
4228
|
+
prompt: input5.userPrompt,
|
|
4229
|
+
schema: ScoutOutputSchema,
|
|
4230
|
+
maxTokens,
|
|
4231
|
+
abortSignal: input5.abort,
|
|
4232
|
+
...anthropicThinking ? { providerOptions: anthropicThinking } : {}
|
|
4233
|
+
});
|
|
4234
|
+
const u = usageFromLanguageModelStep(usage, providerMetadata);
|
|
4235
|
+
input5.budget.recordUsage(u);
|
|
4236
|
+
return {
|
|
4237
|
+
output: object,
|
|
4238
|
+
usage: { inputTokens: u.inputTokens, outputTokens: u.outputTokens }
|
|
4239
|
+
};
|
|
4240
|
+
} catch (e) {
|
|
4241
|
+
if (NoObjectGeneratedError.isInstance(e)) {
|
|
4242
|
+
return null;
|
|
4243
|
+
}
|
|
4244
|
+
if (APICallError.isInstance(e) && attempt === 0) {
|
|
4245
|
+
const status = e.statusCode ?? 0;
|
|
4246
|
+
if (status === 429) {
|
|
4247
|
+
const ra = parseRetryAfterSec(e.responseHeaders ?? {});
|
|
4248
|
+
if (ra !== void 0 && ra <= 90) {
|
|
4249
|
+
await new Promise((r) => setTimeout(r, ra * 1e3));
|
|
4250
|
+
attempt += 1;
|
|
4251
|
+
continue;
|
|
4252
|
+
}
|
|
4253
|
+
}
|
|
4254
|
+
}
|
|
4255
|
+
throw e;
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
// src/planner/validation.ts
|
|
4261
|
+
import fs26 from "fs";
|
|
4262
|
+
import path18 from "path";
|
|
4263
|
+
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;
|
|
4264
|
+
function summariseIssuesByKind(issues) {
|
|
4265
|
+
const out = {};
|
|
4266
|
+
for (const i of issues) {
|
|
4267
|
+
out[i.kind] = (out[i.kind] ?? 0) + 1;
|
|
4268
|
+
}
|
|
4269
|
+
return out;
|
|
4270
|
+
}
|
|
4271
|
+
function extractBacktickPaths(planText) {
|
|
4272
|
+
const claims = [];
|
|
4273
|
+
const re = /`([^`\n]+)`/g;
|
|
4274
|
+
let m;
|
|
4275
|
+
while ((m = re.exec(planText)) !== null) {
|
|
4276
|
+
const inner = m[1].trim();
|
|
4277
|
+
if (!inner.includes("/") || inner.startsWith("http")) continue;
|
|
4278
|
+
if (inner.includes("story ") || inner.includes("Story ")) continue;
|
|
4279
|
+
const seg = inner.split(/[\s:]/)[0];
|
|
4280
|
+
if (!seg || !CODE_EXT.test(seg)) continue;
|
|
4281
|
+
claims.push({ path: seg.replace(/^\.\//, ""), isCreate: false, excerpt: m[0] });
|
|
4282
|
+
}
|
|
4283
|
+
return claims;
|
|
4284
|
+
}
|
|
4285
|
+
function extractFileHeadingPaths(planText) {
|
|
4286
|
+
const claims = [];
|
|
4287
|
+
const fileRe = /\*\*\s*(?:File|Create file)\s*:\s*`([^`\n]+)`\s*\*\*/gi;
|
|
4288
|
+
let m;
|
|
4289
|
+
while ((m = fileRe.exec(planText)) !== null) {
|
|
4290
|
+
const p = m[1].trim().replace(/^\.\//, "");
|
|
4291
|
+
const isCreate = /create file/i.test(m[0]);
|
|
4292
|
+
claims.push({ path: p, isCreate, excerpt: m[0].slice(0, 120) });
|
|
4293
|
+
}
|
|
4294
|
+
return claims;
|
|
4295
|
+
}
|
|
4296
|
+
function mergePathClaims(planText) {
|
|
4297
|
+
const map = /* @__PURE__ */ new Map();
|
|
4298
|
+
for (const c of [...extractFileHeadingPaths(planText), ...extractBacktickPaths(planText)]) {
|
|
4299
|
+
const key = `${c.isCreate}:${c.path}`;
|
|
4300
|
+
if (!map.has(key)) map.set(key, c);
|
|
4301
|
+
}
|
|
4302
|
+
return [...map.values()];
|
|
4303
|
+
}
|
|
4304
|
+
var LINE_RANGE_RE = /(?:~?\s*lines?\s*|lines\s+)(\d+)\s*[–-]\s*(\d+)/gi;
|
|
4305
|
+
var LINE_SINGLE_WORD_RE = /\bline\s+(\d+)\b|\(L(\d+)\)/gi;
|
|
4306
|
+
var PATH_COLON_LINE_RE = /`([^`\n]+\.[a-z0-9]{1,6})`\s*:\s*(\d+)\b/gi;
|
|
4307
|
+
function pathNear(planText, idx) {
|
|
4308
|
+
const before = planText.slice(Math.max(0, idx - 200), idx);
|
|
4309
|
+
const pathMatch = before.match(/`([^`\n]+\.[a-z0-9]{1,6})`/i);
|
|
4310
|
+
const filePath = pathMatch?.[1]?.trim().replace(/^\.\//, "");
|
|
4311
|
+
return filePath;
|
|
4312
|
+
}
|
|
4313
|
+
function trigrams(s) {
|
|
4314
|
+
const x = s.toLowerCase();
|
|
4315
|
+
const g = /* @__PURE__ */ new Set();
|
|
4316
|
+
for (let i = 0; i + 3 <= x.length; i++) {
|
|
4317
|
+
g.add(x.slice(i, i + 3));
|
|
4318
|
+
}
|
|
4319
|
+
return g;
|
|
4320
|
+
}
|
|
4321
|
+
function trigramSimilarity(a, b) {
|
|
4322
|
+
const A = trigrams(a);
|
|
4323
|
+
const B = trigrams(b);
|
|
4324
|
+
if (A.size === 0 || B.size === 0) return 0;
|
|
4325
|
+
let inter = 0;
|
|
4326
|
+
for (const t of A) {
|
|
4327
|
+
if (B.has(t)) inter += 1;
|
|
4328
|
+
}
|
|
4329
|
+
return inter / Math.sqrt(A.size * B.size);
|
|
4330
|
+
}
|
|
4331
|
+
function suggestPaths(root, missingRel, max = 3) {
|
|
4332
|
+
const targetBase = path18.basename(missingRel).toLowerCase();
|
|
4333
|
+
const ig = buildRepoIgnore(root);
|
|
4334
|
+
const candidates = [];
|
|
4335
|
+
const walk2 = (dir) => {
|
|
4336
|
+
if (candidates.length >= 2e3) return;
|
|
4337
|
+
let ents;
|
|
4338
|
+
try {
|
|
4339
|
+
ents = fs26.readdirSync(dir, { withFileTypes: true });
|
|
4340
|
+
} catch {
|
|
4341
|
+
return;
|
|
4342
|
+
}
|
|
4343
|
+
for (const ent of ents) {
|
|
4344
|
+
if (candidates.length >= 2e3) return;
|
|
4345
|
+
const abs = path18.join(dir, ent.name);
|
|
4346
|
+
const rel = path18.relative(root, abs).split(path18.sep).join("/");
|
|
4347
|
+
if (!rel || ig.ignores(rel) || ent.isDirectory() && ig.ignores(`${rel}/`)) continue;
|
|
4348
|
+
if (ent.isDirectory()) walk2(abs);
|
|
4349
|
+
else if (ent.isFile()) candidates.push(rel);
|
|
4350
|
+
}
|
|
4351
|
+
};
|
|
4352
|
+
walk2(root);
|
|
4353
|
+
const rows = [];
|
|
4354
|
+
for (const rel of candidates) {
|
|
4355
|
+
const base = path18.basename(rel).toLowerCase();
|
|
4356
|
+
let score = 0;
|
|
4357
|
+
if (base === targetBase) score = 1e3;
|
|
4358
|
+
else if (base.startsWith(targetBase) || targetBase.startsWith(base)) score = 500;
|
|
4359
|
+
else if (base.includes(targetBase) || targetBase.includes(base)) score = 200;
|
|
4360
|
+
score += trigramSimilarity(targetBase, base) * 50;
|
|
4361
|
+
if (score > 0) rows.push({ rel, score });
|
|
4362
|
+
}
|
|
4363
|
+
rows.sort((a, b) => b.score - a.score || a.rel.localeCompare(b.rel));
|
|
4364
|
+
const uniq = [];
|
|
4365
|
+
for (const r of rows) {
|
|
4366
|
+
if (r.rel === missingRel.replace(/^\.\//, "")) continue;
|
|
4367
|
+
if (!uniq.includes(r.rel)) uniq.push(r.rel);
|
|
4368
|
+
if (uniq.length >= max) break;
|
|
4369
|
+
}
|
|
4370
|
+
return uniq;
|
|
4371
|
+
}
|
|
4372
|
+
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;
|
|
4373
|
+
function collectSymbols(src, maxLines = 200) {
|
|
4374
|
+
const head = src.split(/\r?\n/).slice(0, maxLines).join("\n");
|
|
4375
|
+
const syms = /* @__PURE__ */ new Set();
|
|
4376
|
+
SYMBOL_DEF_RE.lastIndex = 0;
|
|
4377
|
+
let m;
|
|
4378
|
+
while ((m = SYMBOL_DEF_RE.exec(head)) !== null) {
|
|
4379
|
+
syms.add(m[1]);
|
|
4380
|
+
}
|
|
4381
|
+
return [...syms];
|
|
4382
|
+
}
|
|
4383
|
+
function leadingCharScore(sym, missing) {
|
|
4384
|
+
let i = 0;
|
|
4385
|
+
const a = sym.toLowerCase();
|
|
4386
|
+
const b = missing.toLowerCase();
|
|
4387
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
4388
|
+
return i * 10 + trigramSimilarity(sym, missing) * 30;
|
|
4389
|
+
}
|
|
4390
|
+
function suggestSymbols(fileText, missingSym, max = 3) {
|
|
4391
|
+
const defs = collectSymbols(fileText);
|
|
4392
|
+
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));
|
|
4393
|
+
const out = [];
|
|
4394
|
+
for (const r of ranked) {
|
|
4395
|
+
if (!out.includes(r.s)) out.push(r.s);
|
|
4396
|
+
if (out.length >= max) break;
|
|
4397
|
+
}
|
|
4398
|
+
return out;
|
|
4399
|
+
}
|
|
4400
|
+
function parseMetadataLine(planText) {
|
|
4401
|
+
const lines = planText.split(/\r?\n/);
|
|
4402
|
+
for (let i = 0; i < Math.min(5, lines.length); i++) {
|
|
4403
|
+
const line2 = (lines[i] ?? "").trim();
|
|
4404
|
+
if (!line2.startsWith("<!--") || !line2.includes("squad-kit:")) continue;
|
|
4405
|
+
if (!line2.endsWith("-->")) {
|
|
4406
|
+
return {
|
|
4407
|
+
severity: "warning",
|
|
4408
|
+
kind: "malformed_metadata",
|
|
4409
|
+
detail: "Metadata comment is not a single-line <!-- squad-kit: \u2026 -->",
|
|
4410
|
+
excerpt: line2.slice(0, 120)
|
|
4411
|
+
};
|
|
4412
|
+
}
|
|
4413
|
+
const inner = line2.replace(/^<!--\s*squad-kit:\s*/i, "").replace(/\s*-->$/, "");
|
|
4414
|
+
if (!inner.trim()) {
|
|
4415
|
+
return {
|
|
4416
|
+
severity: "warning",
|
|
4417
|
+
kind: "malformed_metadata",
|
|
4418
|
+
detail: "Empty squad-kit metadata body",
|
|
4419
|
+
excerpt: line2
|
|
4420
|
+
};
|
|
4421
|
+
}
|
|
4422
|
+
return void 0;
|
|
4423
|
+
}
|
|
4424
|
+
return void 0;
|
|
4425
|
+
}
|
|
4426
|
+
function validatePlan(input5) {
|
|
4427
|
+
const issues = [];
|
|
4428
|
+
const metaIssue = parseMetadataLine(input5.planText);
|
|
4429
|
+
if (metaIssue) issues.push(metaIssue);
|
|
4430
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4431
|
+
const readCached = (rel) => {
|
|
4432
|
+
if (cache.has(rel)) return cache.get(rel);
|
|
4433
|
+
const resolved = path18.resolve(input5.root, rel);
|
|
4434
|
+
const rc = path18.relative(input5.root, resolved);
|
|
4435
|
+
if (rc.startsWith("..") || path18.isAbsolute(rc)) return void 0;
|
|
4436
|
+
if (!fs26.existsSync(resolved) || !fs26.statSync(resolved).isFile()) return void 0;
|
|
4437
|
+
try {
|
|
4438
|
+
const buf = fs26.readFileSync(resolved);
|
|
4439
|
+
const isBinary = buf.includes(0);
|
|
4440
|
+
const text = isBinary ? "" : buf.toString("utf8");
|
|
4441
|
+
const lines = isBinary ? 0 : text.split(/\r?\n/).length;
|
|
4442
|
+
const entry = { text, lines, isBinary };
|
|
4443
|
+
cache.set(rel, entry);
|
|
4444
|
+
return entry;
|
|
4445
|
+
} catch {
|
|
4446
|
+
return void 0;
|
|
4447
|
+
}
|
|
4448
|
+
};
|
|
4449
|
+
const pathClaims = mergePathClaims(input5.planText);
|
|
4450
|
+
for (const c of pathClaims) {
|
|
4451
|
+
if (c.isCreate) continue;
|
|
4452
|
+
const resolved = path18.resolve(input5.root, c.path);
|
|
4453
|
+
const rel = path18.relative(input5.root, resolved);
|
|
4454
|
+
if (rel.startsWith("..") || path18.isAbsolute(rel)) continue;
|
|
4455
|
+
if (!fs26.existsSync(resolved)) {
|
|
4456
|
+
const suggestions = suggestPaths(input5.root, c.path);
|
|
4457
|
+
issues.push({
|
|
4458
|
+
severity: "warning",
|
|
4459
|
+
kind: "missing_path",
|
|
4460
|
+
path: c.path,
|
|
4461
|
+
detail: "Path cited in plan does not exist in the workspace" + (suggestions.length ? ` \u2014 did you mean: ${suggestions.join(", ")}?` : ""),
|
|
4462
|
+
excerpt: c.excerpt
|
|
4463
|
+
});
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
const rangeText = input5.planText;
|
|
4467
|
+
LINE_RANGE_RE.lastIndex = 0;
|
|
4468
|
+
let rm2;
|
|
4469
|
+
while ((rm2 = LINE_RANGE_RE.exec(rangeText)) !== null) {
|
|
4470
|
+
const lo = parseInt(rm2[1], 10);
|
|
4471
|
+
const hi = parseInt(rm2[2], 10);
|
|
4472
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi)) continue;
|
|
4473
|
+
const filePath = pathNear(rangeText, rm2.index);
|
|
4474
|
+
if (!filePath) continue;
|
|
4475
|
+
const cached = readCached(filePath);
|
|
4476
|
+
const lines = cached?.isBinary ? void 0 : cached?.lines;
|
|
4477
|
+
if (lines !== void 0 && hi > lines) {
|
|
4478
|
+
issues.push({
|
|
4479
|
+
severity: "warning",
|
|
4480
|
+
kind: "line_range_too_large",
|
|
4481
|
+
path: filePath,
|
|
4482
|
+
detail: `Range ends at line ${hi} but file has ${lines} lines`,
|
|
4483
|
+
excerpt: rm2[0].slice(0, 80)
|
|
4484
|
+
});
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
LINE_SINGLE_WORD_RE.lastIndex = 0;
|
|
4488
|
+
while ((rm2 = LINE_SINGLE_WORD_RE.exec(rangeText)) !== null) {
|
|
4489
|
+
const n = parseInt(rm2[1] || rm2[2] || "", 10);
|
|
4490
|
+
if (!Number.isFinite(n)) continue;
|
|
4491
|
+
const filePath = pathNear(rangeText, rm2.index);
|
|
4492
|
+
if (!filePath) continue;
|
|
4493
|
+
const cached = readCached(filePath);
|
|
4494
|
+
const lines = cached?.isBinary ? void 0 : cached?.lines;
|
|
4495
|
+
if (lines !== void 0 && n > lines) {
|
|
4496
|
+
issues.push({
|
|
4497
|
+
severity: "warning",
|
|
4498
|
+
kind: "line_range_too_large",
|
|
4499
|
+
path: filePath,
|
|
4500
|
+
detail: `Range ends at line ${n} but file has ${lines} lines`,
|
|
4501
|
+
excerpt: rm2[0].slice(0, 80)
|
|
4502
|
+
});
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
PATH_COLON_LINE_RE.lastIndex = 0;
|
|
4506
|
+
while ((rm2 = PATH_COLON_LINE_RE.exec(rangeText)) !== null) {
|
|
4507
|
+
const fp = rm2[1].trim().replace(/^\.\//, "");
|
|
4508
|
+
const n = parseInt(rm2[2], 10);
|
|
4509
|
+
if (!Number.isFinite(n)) continue;
|
|
4510
|
+
const cached = readCached(fp);
|
|
4511
|
+
const lines = cached?.isBinary ? void 0 : cached?.lines;
|
|
4512
|
+
if (lines !== void 0 && n > lines) {
|
|
4513
|
+
issues.push({
|
|
4514
|
+
severity: "warning",
|
|
4515
|
+
kind: "line_range_too_large",
|
|
4516
|
+
path: fp,
|
|
4517
|
+
detail: `Range ends at line ${n} but file has ${lines} lines`,
|
|
4518
|
+
excerpt: rm2[0].slice(0, 80)
|
|
4519
|
+
});
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4522
|
+
const textByLine = input5.planText.split(/\r?\n/);
|
|
4523
|
+
for (let li = 0; li < textByLine.length; li++) {
|
|
4524
|
+
const line2 = textByLine[li] ?? "";
|
|
4525
|
+
const near = [textByLine[li - 1] ?? "", line2, textByLine[li + 1] ?? ""].join("\n");
|
|
4526
|
+
const pathNearCell = near.match(/`([^`\n/]+\/[^`\n]+?\.[a-z0-9]{1,6})`/i);
|
|
4527
|
+
if (!pathNearCell) continue;
|
|
4528
|
+
const fp = pathNearCell[1].replace(/^\.\//, "");
|
|
4529
|
+
const cached = readCached(fp);
|
|
4530
|
+
if (!cached || cached.isBinary) continue;
|
|
4531
|
+
const symRe = /`([A-Za-z_][A-Za-z0-9_]{2,50})`/g;
|
|
4532
|
+
let sm;
|
|
4533
|
+
while ((sm = symRe.exec(line2)) !== null) {
|
|
4534
|
+
const sym = sm[1];
|
|
4535
|
+
if (["true", "false", "null", "undefined", "string", "number", "boolean", "void", "never"].includes(
|
|
4536
|
+
sym
|
|
4537
|
+
)) {
|
|
4538
|
+
continue;
|
|
4539
|
+
}
|
|
4540
|
+
if (!cached.text.includes(sym)) {
|
|
4541
|
+
const suggestions = suggestSymbols(cached.text, sym);
|
|
4542
|
+
issues.push({
|
|
4543
|
+
severity: "warning",
|
|
4544
|
+
kind: "symbol_not_found",
|
|
4545
|
+
path: fp,
|
|
4546
|
+
detail: `Symbol \`${sym}\` not found as substring in file` + (suggestions.length ? ` \u2014 did you mean: ${suggestions.map((s) => `\`${s}\``).join(", ")}?` : ""),
|
|
4547
|
+
excerpt: sm[0]
|
|
4548
|
+
});
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
}
|
|
4552
|
+
return issues;
|
|
4553
|
+
}
|
|
4554
|
+
|
|
3876
4555
|
// src/planner/loop.ts
|
|
3877
4556
|
var MAX_RATE_LIMIT_RETRY_SEC = 90;
|
|
3878
4557
|
var defaultSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
@@ -3910,8 +4589,147 @@ function buildPlannerRunStats(budget, turns, runStartedAt) {
|
|
|
3910
4589
|
durationMs: Date.now() - runStartedAt
|
|
3911
4590
|
};
|
|
3912
4591
|
}
|
|
3913
|
-
function
|
|
3914
|
-
|
|
4592
|
+
function buildScoutedContextSection(root, budget, selectedFiles, bus, runId, readRanges) {
|
|
4593
|
+
const normalizeRel = (p) => p.replace(/^\.\//, "");
|
|
4594
|
+
const rangeByPath = /* @__PURE__ */ new Map();
|
|
4595
|
+
for (const rr of readRanges ?? []) {
|
|
4596
|
+
rangeByPath.set(normalizeRel(rr.path), rr);
|
|
4597
|
+
}
|
|
4598
|
+
const ordered = [];
|
|
4599
|
+
const seenPath = /* @__PURE__ */ new Set();
|
|
4600
|
+
for (const f of selectedFiles) {
|
|
4601
|
+
const c = normalizeRel(f);
|
|
4602
|
+
if (!seenPath.has(c)) {
|
|
4603
|
+
seenPath.add(c);
|
|
4604
|
+
ordered.push(c);
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
for (const p of rangeByPath.keys()) {
|
|
4608
|
+
if (!seenPath.has(p)) {
|
|
4609
|
+
seenPath.add(p);
|
|
4610
|
+
ordered.push(p);
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
const blocks = ["\n## Scouted context (already loaded)\n"];
|
|
4614
|
+
for (const cleaned of ordered) {
|
|
4615
|
+
const rr = rangeByPath.get(cleaned);
|
|
4616
|
+
if (rr) {
|
|
4617
|
+
const result = readFileTool(root, budget, { path: cleaned, offset: rr.offset, limit: rr.limit });
|
|
4618
|
+
const hi = rr.offset + rr.limit - 1;
|
|
4619
|
+
if (result.isError) {
|
|
4620
|
+
blocks.push(
|
|
4621
|
+
`
|
|
4622
|
+
### \`${cleaned}\` (lines ${rr.offset}\u2013${hi})
|
|
4623
|
+
|
|
4624
|
+
_${result.content}_
|
|
4625
|
+
`
|
|
4626
|
+
);
|
|
4627
|
+
} else {
|
|
4628
|
+
blocks.push(
|
|
4629
|
+
`
|
|
4630
|
+
### \`${cleaned}\` (lines ${rr.offset}\u2013${hi})
|
|
4631
|
+
|
|
4632
|
+
\`\`\`
|
|
4633
|
+
${result.content}
|
|
4634
|
+
\`\`\`
|
|
4635
|
+
`
|
|
4636
|
+
);
|
|
4637
|
+
}
|
|
4638
|
+
continue;
|
|
4639
|
+
}
|
|
4640
|
+
const resolved = path19.resolve(root, cleaned);
|
|
4641
|
+
const rc = path19.relative(root, resolved);
|
|
4642
|
+
if (rc.startsWith("..") || path19.isAbsolute(rc)) {
|
|
4643
|
+
bus.emit({
|
|
4644
|
+
kind: "validation_issue",
|
|
4645
|
+
runId,
|
|
4646
|
+
severity: "warning",
|
|
4647
|
+
issueKind: "missing_path",
|
|
4648
|
+
path: cleaned,
|
|
4649
|
+
detail: "Scout-selected path escapes the project root"
|
|
4650
|
+
});
|
|
4651
|
+
continue;
|
|
4652
|
+
}
|
|
4653
|
+
if (!fs27.existsSync(resolved)) {
|
|
4654
|
+
bus.emit({
|
|
4655
|
+
kind: "validation_issue",
|
|
4656
|
+
runId,
|
|
4657
|
+
severity: "warning",
|
|
4658
|
+
issueKind: "missing_path",
|
|
4659
|
+
path: cleaned,
|
|
4660
|
+
detail: "Scout selected this path but it does not exist on disk"
|
|
4661
|
+
});
|
|
4662
|
+
continue;
|
|
4663
|
+
}
|
|
4664
|
+
const st = fs27.statSync(resolved);
|
|
4665
|
+
if (!st.isFile()) continue;
|
|
4666
|
+
const n = Math.min(st.size, MAX_BYTES_PER_FULL_READ);
|
|
4667
|
+
const cap = budget.canRead(n);
|
|
4668
|
+
if (!cap.ok) {
|
|
4669
|
+
blocks.push(`
|
|
4670
|
+
### \`${cleaned}\`
|
|
4671
|
+
|
|
4672
|
+
_(skipped: ${cap.reason})_
|
|
4673
|
+
`);
|
|
4674
|
+
continue;
|
|
4675
|
+
}
|
|
4676
|
+
let buf;
|
|
4677
|
+
try {
|
|
4678
|
+
buf = fs27.readFileSync(resolved);
|
|
4679
|
+
} catch {
|
|
4680
|
+
continue;
|
|
4681
|
+
}
|
|
4682
|
+
if (buf.length > MAX_BYTES_PER_FULL_READ) {
|
|
4683
|
+
const head = buf.subarray(0, MAX_BYTES_PER_FULL_READ);
|
|
4684
|
+
if (looksBinary(head)) {
|
|
4685
|
+
blocks.push(`
|
|
4686
|
+
### \`${cleaned}\`
|
|
4687
|
+
|
|
4688
|
+
_(binary file skipped)_
|
|
4689
|
+
`);
|
|
4690
|
+
continue;
|
|
4691
|
+
}
|
|
4692
|
+
budget.recordRead(head.length);
|
|
4693
|
+
blocks.push(
|
|
4694
|
+
`
|
|
4695
|
+
### \`${cleaned}\` (truncated: first ${MAX_BYTES_PER_FULL_READ}-byte head; full size ${buf.length} bytes)
|
|
4696
|
+
|
|
4697
|
+
\`\`\`
|
|
4698
|
+
${head.toString("utf8")}
|
|
4699
|
+
\`\`\`
|
|
4700
|
+
`
|
|
4701
|
+
);
|
|
4702
|
+
continue;
|
|
4703
|
+
}
|
|
4704
|
+
if (looksBinary(buf)) {
|
|
4705
|
+
blocks.push(`
|
|
4706
|
+
### \`${cleaned}\`
|
|
4707
|
+
|
|
4708
|
+
_(binary file skipped)_
|
|
4709
|
+
`);
|
|
4710
|
+
continue;
|
|
4711
|
+
}
|
|
4712
|
+
budget.recordRead(buf.length);
|
|
4713
|
+
blocks.push(`
|
|
4714
|
+
### \`${cleaned}\`
|
|
4715
|
+
|
|
4716
|
+
\`\`\`
|
|
4717
|
+
${buf.toString("utf8")}
|
|
4718
|
+
\`\`\`
|
|
4719
|
+
`);
|
|
4720
|
+
}
|
|
4721
|
+
return blocks.join("");
|
|
4722
|
+
}
|
|
4723
|
+
function scoutStageStats(usage, durationMs) {
|
|
4724
|
+
return {
|
|
4725
|
+
turns: 0,
|
|
4726
|
+
inputTokens: usage.inputTokens,
|
|
4727
|
+
outputTokens: usage.outputTokens,
|
|
4728
|
+
cacheCreationTokens: 0,
|
|
4729
|
+
cacheReadTokens: 0,
|
|
4730
|
+
cacheHitRatio: 0,
|
|
4731
|
+
durationMs
|
|
4732
|
+
};
|
|
3915
4733
|
}
|
|
3916
4734
|
function extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut) {
|
|
3917
4735
|
input5.budget.extendSession();
|
|
@@ -3951,28 +4769,106 @@ function cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, i
|
|
|
3951
4769
|
userCancelled: true
|
|
3952
4770
|
};
|
|
3953
4771
|
}
|
|
3954
|
-
|
|
4772
|
+
function asToolInput(args) {
|
|
4773
|
+
if (args && typeof args === "object" && !Array.isArray(args)) return args;
|
|
4774
|
+
if (typeof args === "string") {
|
|
4775
|
+
try {
|
|
4776
|
+
return JSON.parse(args);
|
|
4777
|
+
} catch {
|
|
4778
|
+
return {};
|
|
4779
|
+
}
|
|
4780
|
+
}
|
|
4781
|
+
return {};
|
|
4782
|
+
}
|
|
4783
|
+
function normalizeToolExecutionResult(result) {
|
|
4784
|
+
if (typeof result === "string") {
|
|
4785
|
+
try {
|
|
4786
|
+
const j = JSON.parse(result);
|
|
4787
|
+
if (j && typeof j === "object" && !Array.isArray(j) && "content" in j) {
|
|
4788
|
+
const o = j;
|
|
4789
|
+
return {
|
|
4790
|
+
content: typeof o.content === "string" ? o.content : JSON.stringify(o.content ?? ""),
|
|
4791
|
+
isError: o.isError === true
|
|
4792
|
+
};
|
|
4793
|
+
}
|
|
4794
|
+
} catch {
|
|
4795
|
+
}
|
|
4796
|
+
return { content: result, isError: false };
|
|
4797
|
+
}
|
|
4798
|
+
if (result && typeof result === "object" && !Array.isArray(result) && "content" in result) {
|
|
4799
|
+
const o = result;
|
|
4800
|
+
return {
|
|
4801
|
+
content: typeof o.content === "string" ? o.content : JSON.stringify(o.content ?? ""),
|
|
4802
|
+
isError: o.isError === true
|
|
4803
|
+
};
|
|
4804
|
+
}
|
|
4805
|
+
return { content: typeof result === "undefined" ? "" : JSON.stringify(result), isError: false };
|
|
4806
|
+
}
|
|
4807
|
+
function applyStepToTurns(turns, step2) {
|
|
4808
|
+
const callsById = /* @__PURE__ */ new Map();
|
|
4809
|
+
const toolCalls = step2.toolCalls.map((tc) => {
|
|
4810
|
+
callsById.set(tc.toolCallId, tc.toolName);
|
|
4811
|
+
return { id: tc.toolCallId, name: tc.toolName, input: asToolInput(tc.args) };
|
|
4812
|
+
});
|
|
4813
|
+
turns.push({
|
|
4814
|
+
role: "assistant",
|
|
4815
|
+
...step2.text ? { text: step2.text } : {},
|
|
4816
|
+
...toolCalls.length ? { toolCalls } : {}
|
|
4817
|
+
});
|
|
4818
|
+
if (step2.toolResults.length) {
|
|
4819
|
+
const toolResults = step2.toolResults.map((tr) => {
|
|
4820
|
+
const norm = normalizeToolExecutionResult(tr.result);
|
|
4821
|
+
return {
|
|
4822
|
+
toolCallId: tr.toolCallId,
|
|
4823
|
+
name: callsById.get(tr.toolCallId) ?? "read_file",
|
|
4824
|
+
content: norm.content,
|
|
4825
|
+
...norm.isError ? { isError: true } : {}
|
|
4826
|
+
};
|
|
4827
|
+
});
|
|
4828
|
+
turns.push({ role: "user", toolResults });
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
function providerResponseFromApiError(provider, modelId, err2) {
|
|
4832
|
+
const body = err2.responseBody ?? "";
|
|
4833
|
+
const nf = detectModelNotFound(provider, modelId, err2.statusCode ?? 0, body);
|
|
4834
|
+
if (nf) {
|
|
4835
|
+
return {
|
|
4836
|
+
stopReason: "error",
|
|
4837
|
+
errorKind: "model_not_found",
|
|
4838
|
+
rawError: modelNotFoundMessage(nf)
|
|
4839
|
+
};
|
|
4840
|
+
}
|
|
4841
|
+
const rl = detectRateLimit(provider, err2.statusCode ?? 0, err2.responseHeaders ?? {}, body);
|
|
4842
|
+
if (rl) {
|
|
4843
|
+
return {
|
|
4844
|
+
stopReason: "error",
|
|
4845
|
+
errorKind: "rate_limit",
|
|
4846
|
+
retryAfterSec: rl.retryAfterSec,
|
|
4847
|
+
rawError: `${provider} ${err2.statusCode ?? "error"}: ${body.slice(0, 500)}`
|
|
4848
|
+
};
|
|
4849
|
+
}
|
|
4850
|
+
return {
|
|
4851
|
+
stopReason: "error",
|
|
4852
|
+
rawError: `${provider} ${err2.statusCode ?? "error"}: ${body.slice(0, 500)}`
|
|
4853
|
+
};
|
|
4854
|
+
}
|
|
4855
|
+
async function runDraftStage(input5) {
|
|
3955
4856
|
const runStartedAt = Date.now();
|
|
3956
4857
|
const bus = input5.events ?? new PlannerEventBus();
|
|
3957
4858
|
const runId = input5.runId ?? newRunId();
|
|
3958
4859
|
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
4860
|
const baseMaxIter = input5.maxIterations ?? DEFAULT_PLANNER_MAX_ITERATIONS;
|
|
3967
4861
|
const maxIter = { current: baseMaxIter };
|
|
3968
4862
|
const baseMaxOut = input5.maxOutputTokens ?? DEFAULT_PLANNER_MAX_OUTPUT_TOKENS;
|
|
3969
4863
|
const maxOut = { current: baseMaxOut };
|
|
3970
4864
|
const turns = [{ role: "user", text: input5.userPrompt }];
|
|
4865
|
+
if (input5.scoutedSection?.trim()) {
|
|
4866
|
+
turns.push({ role: "user", text: composeScoutedUserMessage({ scoutedSection: input5.scoutedSection }) });
|
|
4867
|
+
}
|
|
3971
4868
|
let accumulatedText = "";
|
|
3972
4869
|
let iterations = 0;
|
|
3973
4870
|
let budgetExhausted = false;
|
|
3974
4871
|
let finishedNormally = false;
|
|
3975
|
-
let prevRequestPrefix;
|
|
3976
4872
|
const decide = input5.decideOnLimit;
|
|
3977
4873
|
const sleepFn = input5.sleep ?? defaultSleep;
|
|
3978
4874
|
const limitCtx = (kind) => ({
|
|
@@ -4043,126 +4939,169 @@ async function runPlanner(input5) {
|
|
|
4043
4939
|
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
4044
4940
|
};
|
|
4045
4941
|
}
|
|
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
4942
|
if (iterations >= 2) {
|
|
4075
4943
|
bus.emit({ kind: "turn_started", runId, turn: iterations });
|
|
4076
4944
|
}
|
|
4077
|
-
const
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4945
|
+
const tools = buildPlannerTools({
|
|
4946
|
+
root: input5.root,
|
|
4947
|
+
budget: input5.budget,
|
|
4948
|
+
enabled: {
|
|
4949
|
+
grep: input5.toolsEnabled?.grep ?? true,
|
|
4950
|
+
listDir: input5.toolsEnabled?.listDir ?? true,
|
|
4951
|
+
rangedRead: input5.toolsEnabled?.rangedRead ?? true
|
|
4952
|
+
},
|
|
4953
|
+
readHooks: {
|
|
4954
|
+
runId,
|
|
4955
|
+
turn: iterations,
|
|
4956
|
+
bus,
|
|
4957
|
+
onToolCall: input5.onToolCall,
|
|
4958
|
+
decideOnLimit: input5.decideOnLimit,
|
|
4959
|
+
getLimitCtx: () => limitCtx("file_or_context_reads"),
|
|
4960
|
+
extendSessionLimits: () => extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut),
|
|
4961
|
+
getAccumulatedText: () => accumulatedText,
|
|
4962
|
+
setBudgetExhausted: (v) => {
|
|
4963
|
+
budgetExhausted = v;
|
|
4964
|
+
}
|
|
4965
|
+
}
|
|
4966
|
+
});
|
|
4967
|
+
const messages = turnsToCoreMessages(input5.systemPrompt, turns, {
|
|
4084
4968
|
cacheEnabled,
|
|
4085
|
-
|
|
4086
|
-
};
|
|
4969
|
+
provider: input5.provider
|
|
4970
|
+
});
|
|
4087
4971
|
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;
|
|
4972
|
+
let attempt = 0;
|
|
4973
|
+
let stepResult;
|
|
4110
4974
|
let retrySkippedReason;
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
const
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
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);
|
|
4975
|
+
let retriedRateLimit = false;
|
|
4976
|
+
const callStream = async () => {
|
|
4977
|
+
const { streamText } = await import("ai");
|
|
4978
|
+
const stream = streamText({
|
|
4979
|
+
model: input5.model,
|
|
4980
|
+
messages,
|
|
4981
|
+
tools,
|
|
4982
|
+
toolChoice: "auto",
|
|
4983
|
+
maxSteps: 1,
|
|
4984
|
+
maxRetries: 0,
|
|
4985
|
+
maxTokens: maxOut.current,
|
|
4986
|
+
abortSignal: input5.abort,
|
|
4987
|
+
// ai@4.x defaults `temperature: 0` (prepareCallSettings) and @ai-sdk/anthropic@1.x
|
|
4988
|
+
// always passes it to the wire unless extended thinking is on. Anthropic Opus 4.7+
|
|
4989
|
+
// rejects temperature entirely. Enabling thinking on Anthropic strips temperature
|
|
4990
|
+
// via the adapter and gives the planner reasoning budget for free.
|
|
4991
|
+
...input5.provider === "anthropic" && {
|
|
4992
|
+
providerOptions: {
|
|
4993
|
+
anthropic: { thinking: { type: "enabled", budgetTokens: 2048 } }
|
|
4138
4994
|
}
|
|
4139
|
-
|
|
4995
|
+
},
|
|
4996
|
+
onChunk: ({ chunk }) => {
|
|
4997
|
+
if (chunk.type === "text-delta") {
|
|
4998
|
+
accumulatedText += chunk.textDelta;
|
|
4999
|
+
input5.onAssistantText?.(chunk.textDelta);
|
|
5000
|
+
bus.emit({ kind: "assistant_text", runId, turn: iterations, delta: chunk.textDelta });
|
|
5001
|
+
}
|
|
5002
|
+
},
|
|
5003
|
+
onStepFinish: (step2) => {
|
|
5004
|
+
const u = usageFromLanguageModelStep(step2.usage, step2.providerMetadata);
|
|
5005
|
+
recordUsageAndEmit(bus, input5, runId, iterations, u);
|
|
5006
|
+
}
|
|
5007
|
+
});
|
|
5008
|
+
for await (const part of stream.fullStream) {
|
|
5009
|
+
const p = part;
|
|
5010
|
+
if (p.type === "error") {
|
|
5011
|
+
throw p.error;
|
|
4140
5012
|
}
|
|
5013
|
+
}
|
|
5014
|
+
const steps = await stream.steps;
|
|
5015
|
+
const last = steps[steps.length - 1];
|
|
5016
|
+
if (!last) throw new Error("planner: empty model step");
|
|
5017
|
+
stepResult = {
|
|
5018
|
+
text: last.text,
|
|
5019
|
+
finishReason: last.finishReason,
|
|
5020
|
+
toolCalls: last.toolCalls.flatMap(
|
|
5021
|
+
(tc) => tc ? [
|
|
5022
|
+
{
|
|
5023
|
+
toolCallId: tc.toolCallId,
|
|
5024
|
+
toolName: tc.toolName,
|
|
5025
|
+
args: tc.args
|
|
5026
|
+
}
|
|
5027
|
+
] : []
|
|
5028
|
+
),
|
|
5029
|
+
toolResults: last.toolResults.flatMap(
|
|
5030
|
+
(tr) => tr ? [{ toolCallId: tr.toolCallId, result: tr.result }] : []
|
|
5031
|
+
),
|
|
5032
|
+
usage: last.usage,
|
|
5033
|
+
providerMetadata: last.providerMetadata
|
|
5034
|
+
};
|
|
5035
|
+
};
|
|
5036
|
+
try {
|
|
5037
|
+
while (true) {
|
|
4141
5038
|
try {
|
|
4142
|
-
|
|
5039
|
+
await callStream();
|
|
5040
|
+
break;
|
|
4143
5041
|
} catch (e) {
|
|
5042
|
+
if (PlannerUserCancelledError.is(e)) {
|
|
5043
|
+
return {
|
|
5044
|
+
planText: e.planText,
|
|
5045
|
+
budgetExhausted: false,
|
|
5046
|
+
timedOut: false,
|
|
5047
|
+
finishedNormally: false,
|
|
5048
|
+
iterations,
|
|
5049
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
|
|
5050
|
+
incompleteKind: "budget_reads",
|
|
5051
|
+
userCancelled: true
|
|
5052
|
+
};
|
|
5053
|
+
}
|
|
4144
5054
|
if (isAbortError(e)) {
|
|
4145
5055
|
return cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, iterations, runStartedAt);
|
|
4146
5056
|
}
|
|
5057
|
+
if (APICallError2.isInstance(e)) {
|
|
5058
|
+
const synthetic = providerResponseFromApiError(input5.provider, input5.modelId, e);
|
|
5059
|
+
if (synthetic.errorKind === "rate_limit") {
|
|
5060
|
+
const asked = synthetic.retryAfterSec;
|
|
5061
|
+
const emitRateLimit = (phase, waitSec) => {
|
|
5062
|
+
bus.emit({
|
|
5063
|
+
kind: "rate_limit",
|
|
5064
|
+
runId,
|
|
5065
|
+
turn: iterations,
|
|
5066
|
+
retryAfterSec: synthetic.retryAfterSec,
|
|
5067
|
+
waitSec,
|
|
5068
|
+
capSec: MAX_RATE_LIMIT_RETRY_SEC,
|
|
5069
|
+
phase,
|
|
5070
|
+
provider: input5.provider,
|
|
5071
|
+
rawBody: (synthetic.rawError ?? "").slice(0, 200)
|
|
5072
|
+
});
|
|
5073
|
+
};
|
|
5074
|
+
if (asked !== void 0 && asked > MAX_RATE_LIMIT_RETRY_SEC) {
|
|
5075
|
+
emitRateLimit("aborted", asked);
|
|
5076
|
+
retrySkippedReason = "retry_after_too_long";
|
|
5077
|
+
throw composePlannerError(input5.provider, synthetic, false, retrySkippedReason);
|
|
5078
|
+
}
|
|
5079
|
+
if (attempt === 0) {
|
|
5080
|
+
const waitSec = Math.min(asked ?? 10, MAX_RATE_LIMIT_RETRY_SEC);
|
|
5081
|
+
emitRateLimit("retrying", waitSec);
|
|
5082
|
+
input5.onRateLimit?.(waitSec);
|
|
5083
|
+
await sleepWithAbort(waitSec * 1e3, sleepFn, input5.abort);
|
|
5084
|
+
attempt += 1;
|
|
5085
|
+
retriedRateLimit = true;
|
|
5086
|
+
continue;
|
|
5087
|
+
}
|
|
5088
|
+
throw composePlannerError(input5.provider, synthetic, true, retrySkippedReason);
|
|
5089
|
+
}
|
|
5090
|
+
throw composePlannerError(input5.provider, synthetic, retriedRateLimit, retrySkippedReason);
|
|
5091
|
+
}
|
|
4147
5092
|
throw e;
|
|
4148
5093
|
}
|
|
4149
|
-
if (response.usage) {
|
|
4150
|
-
recordUsageAndEmit(bus, input5, runId, iterations, response.usage);
|
|
4151
|
-
}
|
|
4152
|
-
retriedRateLimit = true;
|
|
4153
5094
|
}
|
|
5095
|
+
} catch (e) {
|
|
5096
|
+
if (isAbortError(e)) {
|
|
5097
|
+
return cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, iterations, runStartedAt);
|
|
5098
|
+
}
|
|
5099
|
+
throw e;
|
|
4154
5100
|
}
|
|
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 });
|
|
5101
|
+
const finishReason = stepResult.finishReason;
|
|
5102
|
+
const toolCallsLen = stepResult.toolCalls.length;
|
|
5103
|
+
if (finishReason === "length" && toolCallsLen === 0) {
|
|
5104
|
+
bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: "max_tokens" });
|
|
4166
5105
|
if (decide) {
|
|
4167
5106
|
const d = await decide(limitCtx("max_output_tokens"));
|
|
4168
5107
|
if (d === "cancel") {
|
|
@@ -4178,7 +5117,7 @@ async function runPlanner(input5) {
|
|
|
4178
5117
|
};
|
|
4179
5118
|
}
|
|
4180
5119
|
extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut);
|
|
4181
|
-
turns
|
|
5120
|
+
applyStepToTurns(turns, stepResult);
|
|
4182
5121
|
turns.push({ role: "user", text: PLANNER_MARKDOWN_CONTINUATION_USER });
|
|
4183
5122
|
iterations -= 1;
|
|
4184
5123
|
continue;
|
|
@@ -4193,8 +5132,13 @@ async function runPlanner(input5) {
|
|
|
4193
5132
|
incompleteKind: "max_output_tokens"
|
|
4194
5133
|
};
|
|
4195
5134
|
}
|
|
4196
|
-
if (
|
|
4197
|
-
|
|
5135
|
+
if (finishReason === "error" || finishReason === "content-filter") {
|
|
5136
|
+
throw new Error(
|
|
5137
|
+
stepResult.text || `planner: model reported ${finishReason}. Run \`squad doctor\` to diagnose, or retry \u2014 most 5xx errors are transient.`
|
|
5138
|
+
);
|
|
5139
|
+
}
|
|
5140
|
+
if ((finishReason === "stop" || finishReason === "unknown") && toolCallsLen === 0) {
|
|
5141
|
+
bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
|
|
4198
5142
|
finishedNormally = true;
|
|
4199
5143
|
return {
|
|
4200
5144
|
planText: accumulatedText,
|
|
@@ -4205,81 +5149,174 @@ async function runPlanner(input5) {
|
|
|
4205
5149
|
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
4206
5150
|
};
|
|
4207
5151
|
}
|
|
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
|
|
5152
|
+
if (toolCallsLen === 0) {
|
|
5153
|
+
bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
|
|
5154
|
+
finishedNormally = true;
|
|
5155
|
+
return {
|
|
5156
|
+
planText: accumulatedText,
|
|
5157
|
+
budgetExhausted,
|
|
5158
|
+
timedOut: false,
|
|
5159
|
+
finishedNormally,
|
|
5160
|
+
iterations,
|
|
5161
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
5162
|
+
};
|
|
5163
|
+
}
|
|
5164
|
+
applyStepToTurns(turns, stepResult);
|
|
5165
|
+
if (budgetExhausted) {
|
|
5166
|
+
turns.push({
|
|
5167
|
+
role: "user",
|
|
5168
|
+
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
5169
|
});
|
|
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
|
-
|
|
5170
|
+
}
|
|
5171
|
+
bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
|
|
5172
|
+
}
|
|
5173
|
+
}
|
|
5174
|
+
async function runPlanner(input5) {
|
|
5175
|
+
const bus = input5.events ?? new PlannerEventBus();
|
|
5176
|
+
const runId = input5.runId ?? newRunId();
|
|
5177
|
+
const cacheEnabled = input5.cacheEnabled ?? true;
|
|
5178
|
+
bus.emit({
|
|
5179
|
+
kind: "started",
|
|
5180
|
+
runId,
|
|
5181
|
+
provider: input5.provider,
|
|
5182
|
+
model: input5.modelId,
|
|
5183
|
+
cacheEnabled
|
|
5184
|
+
});
|
|
5185
|
+
const merged = { ...input5, events: bus, runId };
|
|
5186
|
+
const systemPrompt = input5.systemPrompt;
|
|
5187
|
+
let scoutSummary;
|
|
5188
|
+
let scoutStats;
|
|
5189
|
+
let scoutedSection;
|
|
5190
|
+
const scoutOn = input5.stages?.scout?.enabled !== false;
|
|
5191
|
+
const scoutModel = input5.stages?.scout?.model;
|
|
5192
|
+
const maxScoutFiles = input5.stages?.scout?.maxFiles ?? 12;
|
|
5193
|
+
const scoutMaxOut = input5.stages?.scout?.maxOutputTokens ?? 2048;
|
|
5194
|
+
const scoutModelIdStr = input5.stages?.scout?.modelId ?? "";
|
|
5195
|
+
if (scoutOn && scoutModel) {
|
|
5196
|
+
if (!input5.scoutSystemPrompt?.trim()) {
|
|
5197
|
+
throw new Error(
|
|
5198
|
+
"planner: scout enabled but `scoutSystemPrompt` was not provided. Compose it via composeScoutSystemPrompt() or set stages.scout.enabled = false."
|
|
5199
|
+
);
|
|
5200
|
+
}
|
|
5201
|
+
const t0 = Date.now();
|
|
5202
|
+
bus.emit({ kind: "stage_started", runId, stage: "scout" });
|
|
5203
|
+
try {
|
|
5204
|
+
const scoutRes = await runScout({
|
|
5205
|
+
model: scoutModel,
|
|
5206
|
+
provider: input5.provider,
|
|
5207
|
+
systemPrompt: input5.scoutSystemPrompt,
|
|
5208
|
+
userPrompt: input5.userPrompt,
|
|
5209
|
+
budget: input5.budget,
|
|
5210
|
+
abort: input5.abort,
|
|
5211
|
+
maxTokens: scoutMaxOut
|
|
5212
|
+
});
|
|
5213
|
+
const durationMs = Date.now() - t0;
|
|
5214
|
+
if (scoutRes) {
|
|
5215
|
+
const files = scoutRes.output.selectedFiles.slice(0, maxScoutFiles);
|
|
5216
|
+
const tokensUsed = scoutRes.usage.inputTokens + scoutRes.usage.outputTokens;
|
|
5217
|
+
scoutSummary = {
|
|
5218
|
+
selected: files,
|
|
5219
|
+
reasoning: scoutRes.output.reasoning,
|
|
5220
|
+
durationMs,
|
|
5221
|
+
tokensUsed
|
|
5222
|
+
};
|
|
5223
|
+
scoutStats = scoutStageStats(scoutRes.usage, durationMs);
|
|
5224
|
+
bus.emit({
|
|
5225
|
+
kind: "scout_result",
|
|
5226
|
+
runId,
|
|
5227
|
+
selected: files,
|
|
5228
|
+
reasoning: scoutRes.output.reasoning
|
|
5229
|
+
});
|
|
5230
|
+
scoutedSection = buildScoutedContextSection(
|
|
5231
|
+
input5.root,
|
|
5232
|
+
input5.budget,
|
|
5233
|
+
files,
|
|
5234
|
+
bus,
|
|
5235
|
+
runId,
|
|
5236
|
+
scoutRes.output.readRanges
|
|
5237
|
+
);
|
|
5238
|
+
bus.emit({
|
|
5239
|
+
kind: "stage_complete",
|
|
5240
|
+
runId,
|
|
5241
|
+
stage: "scout",
|
|
5242
|
+
success: true,
|
|
5243
|
+
durationMs,
|
|
5244
|
+
tokensUsed
|
|
5245
|
+
});
|
|
5246
|
+
} else {
|
|
5247
|
+
bus.emit({
|
|
5248
|
+
kind: "stage_complete",
|
|
5249
|
+
runId,
|
|
5250
|
+
stage: "scout",
|
|
5251
|
+
success: false,
|
|
5252
|
+
durationMs
|
|
5253
|
+
});
|
|
5254
|
+
}
|
|
5255
|
+
} catch (err2) {
|
|
5256
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
5257
|
+
if (APICallError2.isInstance(err2)) {
|
|
5258
|
+
const status = err2.statusCode ?? 0;
|
|
5259
|
+
if (status === 401 || status === 403) {
|
|
5260
|
+
throw err2;
|
|
5261
|
+
}
|
|
5262
|
+
if (detectModelNotFound(input5.provider, scoutModelIdStr, status, err2.responseBody ?? "")) {
|
|
5263
|
+
throw err2;
|
|
4263
5264
|
}
|
|
4264
5265
|
}
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
5266
|
+
bus.emit({
|
|
5267
|
+
kind: "stage_complete",
|
|
5268
|
+
runId,
|
|
5269
|
+
stage: "scout",
|
|
5270
|
+
success: false,
|
|
5271
|
+
durationMs: Date.now() - t0,
|
|
5272
|
+
errorMessage: msg.slice(0, 200)
|
|
4272
5273
|
});
|
|
4273
5274
|
}
|
|
4274
|
-
bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: response.stopReason });
|
|
4275
5275
|
}
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
const
|
|
4279
|
-
|
|
4280
|
-
|
|
5276
|
+
const draftStarted = Date.now();
|
|
5277
|
+
bus.emit({ kind: "stage_started", runId, stage: "draft" });
|
|
5278
|
+
const draft = await runDraftStage({ ...merged, systemPrompt, scoutedSection });
|
|
5279
|
+
bus.emit({
|
|
5280
|
+
kind: "stage_complete",
|
|
5281
|
+
runId,
|
|
5282
|
+
stage: "draft",
|
|
5283
|
+
success: draft.finishedNormally && !draft.timedOut && !draft.userCancelled,
|
|
5284
|
+
durationMs: Date.now() - draftStarted
|
|
5285
|
+
});
|
|
5286
|
+
const validationOn = input5.validation?.enabled !== false;
|
|
5287
|
+
let issues = [];
|
|
5288
|
+
let validationDurationMs = 0;
|
|
5289
|
+
if (validationOn && draft.planText.trim()) {
|
|
5290
|
+
const v0 = Date.now();
|
|
5291
|
+
bus.emit({ kind: "stage_started", runId, stage: "validation" });
|
|
5292
|
+
issues = validatePlan({ root: input5.root, planText: draft.planText });
|
|
5293
|
+
for (const iss of issues.slice(0, 100)) {
|
|
5294
|
+
bus.emit({
|
|
5295
|
+
kind: "validation_issue",
|
|
5296
|
+
runId,
|
|
5297
|
+
severity: iss.severity,
|
|
5298
|
+
issueKind: iss.kind,
|
|
5299
|
+
path: iss.path,
|
|
5300
|
+
detail: iss.detail,
|
|
5301
|
+
excerpt: iss.excerpt
|
|
5302
|
+
});
|
|
5303
|
+
}
|
|
5304
|
+
validationDurationMs = Date.now() - v0;
|
|
5305
|
+
bus.emit({
|
|
5306
|
+
kind: "stage_complete",
|
|
5307
|
+
runId,
|
|
5308
|
+
stage: "validation",
|
|
5309
|
+
success: true,
|
|
5310
|
+
durationMs: validationDurationMs
|
|
5311
|
+
});
|
|
4281
5312
|
}
|
|
4282
|
-
return
|
|
5313
|
+
return {
|
|
5314
|
+
...draft,
|
|
5315
|
+
stats: draft.stats,
|
|
5316
|
+
stagesStats: { scout: scoutStats, draft: draft.stats },
|
|
5317
|
+
scout: scoutSummary,
|
|
5318
|
+
validation: validationOn ? { issues, durationMs: validationDurationMs } : { issues: [], durationMs: 0 }
|
|
5319
|
+
};
|
|
4283
5320
|
}
|
|
4284
5321
|
function composePlannerError(providerName, response, retriedRateLimit, retrySkippedReason) {
|
|
4285
5322
|
if (response.errorKind === "rate_limit") {
|
|
@@ -4318,45 +5355,39 @@ function kindTitle(kind) {
|
|
|
4318
5355
|
return "Model round limit";
|
|
4319
5356
|
case "wall_clock":
|
|
4320
5357
|
return "Wall-clock time limit";
|
|
4321
|
-
case "cost_cap":
|
|
4322
|
-
return "Estimated cost cap";
|
|
4323
5358
|
case "file_or_context_reads":
|
|
4324
5359
|
return "File read or context size limit";
|
|
4325
5360
|
}
|
|
4326
5361
|
}
|
|
4327
|
-
function
|
|
5362
|
+
function tokenSummary(ctx) {
|
|
4328
5363
|
const snap = ctx.budgetSnapshot;
|
|
4329
|
-
|
|
5364
|
+
return `Tokens so far: ${snap.usage.inputTokens} in / ${snap.usage.outputTokens} out for this session.`;
|
|
5365
|
+
}
|
|
5366
|
+
function kindDetail(ctx) {
|
|
4330
5367
|
switch (ctx.kind) {
|
|
4331
5368
|
case "max_output_tokens":
|
|
4332
5369
|
return [
|
|
4333
5370
|
`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
5371
|
"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
|
-
|
|
5372
|
+
tokenSummary(ctx)
|
|
4336
5373
|
];
|
|
4337
5374
|
case "max_iterations":
|
|
4338
5375
|
return [
|
|
4339
5376
|
`The planner reached its round cap (${ctx.maxIterations} model turns) without a clean stop.`,
|
|
4340
5377
|
"Continuing raises the round cap and file/time budgets for this run by another full slice from your config.",
|
|
4341
|
-
|
|
5378
|
+
tokenSummary(ctx)
|
|
4342
5379
|
];
|
|
4343
5380
|
case "wall_clock":
|
|
4344
5381
|
return [
|
|
4345
5382
|
"The configured wall-clock budget for this planning session elapsed before the model finished.",
|
|
4346
5383
|
"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
|
|
5384
|
+
tokenSummary(ctx)
|
|
4354
5385
|
];
|
|
4355
5386
|
case "file_or_context_reads":
|
|
4356
5387
|
return [
|
|
4357
5388
|
"A `read_file` call could not run because the file-read count or total read-bytes budget was exhausted.",
|
|
4358
5389
|
"Continuing extends read, context, time, round, and output limits for this session so the model can read again or finish without reads.",
|
|
4359
|
-
|
|
5390
|
+
tokenSummary(ctx)
|
|
4360
5391
|
];
|
|
4361
5392
|
}
|
|
4362
5393
|
}
|
|
@@ -4380,14 +5411,17 @@ function printPlannerLimitNextSteps() {
|
|
|
4380
5411
|
}
|
|
4381
5412
|
|
|
4382
5413
|
// src/planner/providers/index.ts
|
|
4383
|
-
|
|
4384
|
-
|
|
5414
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
5415
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
5416
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
5417
|
+
function resolveModel(provider, modelId, apiKey) {
|
|
5418
|
+
switch (provider) {
|
|
4385
5419
|
case "anthropic":
|
|
4386
|
-
return
|
|
5420
|
+
return { provider, model: createAnthropic({ apiKey })(modelId), modelId };
|
|
4387
5421
|
case "openai":
|
|
4388
|
-
return
|
|
5422
|
+
return { provider, model: createOpenAI({ apiKey })(modelId), modelId };
|
|
4389
5423
|
case "google":
|
|
4390
|
-
return
|
|
5424
|
+
return { provider, model: createGoogleGenerativeAI({ apiKey })(modelId), modelId };
|
|
4391
5425
|
}
|
|
4392
5426
|
}
|
|
4393
5427
|
|
|
@@ -4398,7 +5432,6 @@ var Budget = class {
|
|
|
4398
5432
|
this.maxFileReadsAllowed = cfg.maxFileReads;
|
|
4399
5433
|
this.maxContextBytesAllowed = cfg.maxContextBytes;
|
|
4400
5434
|
this.maxWallSeconds = cfg.maxDurationSeconds;
|
|
4401
|
-
this.maxCostUsdCeiling = cfg.maxCostUsd;
|
|
4402
5435
|
}
|
|
4403
5436
|
cfg;
|
|
4404
5437
|
reads = 0;
|
|
@@ -4408,15 +5441,11 @@ var Budget = class {
|
|
|
4408
5441
|
maxFileReadsAllowed;
|
|
4409
5442
|
maxContextBytesAllowed;
|
|
4410
5443
|
maxWallSeconds;
|
|
4411
|
-
maxCostUsdCeiling;
|
|
4412
5444
|
/** Add another slice of the original config limits (used when the user chooses to continue a planner session). */
|
|
4413
5445
|
extendSession() {
|
|
4414
5446
|
this.maxFileReadsAllowed += this.cfg.maxFileReads;
|
|
4415
5447
|
this.maxContextBytesAllowed += this.cfg.maxContextBytes;
|
|
4416
5448
|
this.maxWallSeconds += this.cfg.maxDurationSeconds;
|
|
4417
|
-
if (this.cfg.maxCostUsd !== void 0) {
|
|
4418
|
-
this.maxCostUsdCeiling = (this.maxCostUsdCeiling ?? 0) + this.cfg.maxCostUsd;
|
|
4419
|
-
}
|
|
4420
5449
|
}
|
|
4421
5450
|
canRead(nextFileBytes) {
|
|
4422
5451
|
if (this.reads >= this.maxFileReadsAllowed) {
|
|
@@ -4427,6 +5456,13 @@ var Budget = class {
|
|
|
4427
5456
|
}
|
|
4428
5457
|
return { ok: true };
|
|
4429
5458
|
}
|
|
5459
|
+
/** Whether another tool invocation is allowed under the read-count budget (bytes checked per tool). */
|
|
5460
|
+
canExecuteTool() {
|
|
5461
|
+
if (this.reads >= this.maxFileReadsAllowed) {
|
|
5462
|
+
return { ok: false, reason: `max file reads (${this.maxFileReadsAllowed}) reached` };
|
|
5463
|
+
}
|
|
5464
|
+
return { ok: true };
|
|
5465
|
+
}
|
|
4430
5466
|
recordRead(bytes) {
|
|
4431
5467
|
this.reads += 1;
|
|
4432
5468
|
this.bytes += bytes;
|
|
@@ -4434,17 +5470,12 @@ var Budget = class {
|
|
|
4434
5470
|
recordUsage(u) {
|
|
4435
5471
|
this.totalUsage.inputTokens += u.inputTokens;
|
|
4436
5472
|
this.totalUsage.outputTokens += u.outputTokens;
|
|
4437
|
-
if (u.costUsd !== void 0) this.totalUsage.costUsd = (this.totalUsage.costUsd ?? 0) + u.costUsd;
|
|
4438
5473
|
this.totalUsage.cacheCreationTokens = (this.totalUsage.cacheCreationTokens ?? 0) + (u.cacheCreationTokens ?? 0);
|
|
4439
5474
|
this.totalUsage.cacheReadTokens = (this.totalUsage.cacheReadTokens ?? 0) + (u.cacheReadTokens ?? 0);
|
|
4440
5475
|
}
|
|
4441
5476
|
timedOut() {
|
|
4442
5477
|
return (Date.now() - this.startedAt) / 1e3 > this.maxWallSeconds;
|
|
4443
5478
|
}
|
|
4444
|
-
overCost() {
|
|
4445
|
-
if (this.maxCostUsdCeiling === void 0) return false;
|
|
4446
|
-
return (this.totalUsage.costUsd ?? 0) > this.maxCostUsdCeiling;
|
|
4447
|
-
}
|
|
4448
5479
|
snapshot() {
|
|
4449
5480
|
return {
|
|
4450
5481
|
reads: this.reads,
|
|
@@ -4455,103 +5486,9 @@ var Budget = class {
|
|
|
4455
5486
|
}
|
|
4456
5487
|
};
|
|
4457
5488
|
|
|
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
5489
|
// src/planner/writer.ts
|
|
4553
|
-
import
|
|
4554
|
-
import
|
|
5490
|
+
import fs28 from "fs";
|
|
5491
|
+
import path20 from "path";
|
|
4555
5492
|
function planFileSlugParts(config2, story) {
|
|
4556
5493
|
const baseSlug = slugify(story.id);
|
|
4557
5494
|
const trackerId = config2.naming.includeTrackerId && config2.tracker.type !== "none" ? trackerIdForFilename(config2.tracker.type, story.id) : "";
|
|
@@ -4560,8 +5497,8 @@ function planFileSlugParts(config2, story) {
|
|
|
4560
5497
|
}
|
|
4561
5498
|
function writePlanFile(input5) {
|
|
4562
5499
|
const { paths, config: config2, story } = input5;
|
|
4563
|
-
const featurePlanDir =
|
|
4564
|
-
|
|
5500
|
+
const featurePlanDir = path20.join(paths.plansDir, story.feature);
|
|
5501
|
+
fs28.mkdirSync(featurePlanDir, { recursive: true });
|
|
4565
5502
|
if (input5.partial) {
|
|
4566
5503
|
return writePartialPlanFile(input5, featurePlanDir, paths, config2, story);
|
|
4567
5504
|
}
|
|
@@ -4576,14 +5513,14 @@ function writePlanFile(input5) {
|
|
|
4576
5513
|
} else {
|
|
4577
5514
|
nn = scanPlans(paths.plansDir).nextGlobal;
|
|
4578
5515
|
const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
|
|
4579
|
-
targetPath =
|
|
5516
|
+
targetPath = path20.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.md`);
|
|
4580
5517
|
}
|
|
4581
5518
|
const body = `${input5.metadataHeader.trimEnd()}
|
|
4582
5519
|
|
|
4583
5520
|
${input5.planBodyMarkdown.trimStart()}
|
|
4584
5521
|
`;
|
|
4585
|
-
|
|
4586
|
-
upsertOverviewRow(featurePlanDir, story.feature, nn,
|
|
5522
|
+
fs28.writeFileSync(targetPath, body, "utf8");
|
|
5523
|
+
upsertOverviewRow(featurePlanDir, story.feature, nn, path20.basename(targetPath), story, false);
|
|
4587
5524
|
return { planFile: targetPath, sequenceNumber: nn, overwrote };
|
|
4588
5525
|
}
|
|
4589
5526
|
function writePartialPlanFile(input5, featurePlanDir, paths, config2, story) {
|
|
@@ -4598,42 +5535,42 @@ function writePartialPlanFile(input5, featurePlanDir, paths, config2, story) {
|
|
|
4598
5535
|
} else {
|
|
4599
5536
|
nn = scanPlans(paths.plansDir).nextGlobal;
|
|
4600
5537
|
const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
|
|
4601
|
-
targetPath =
|
|
5538
|
+
targetPath = path20.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.partial.md`);
|
|
4602
5539
|
}
|
|
4603
5540
|
const body = `${input5.metadataHeader.trimEnd()}
|
|
4604
5541
|
|
|
4605
5542
|
${input5.planBodyMarkdown.trimStart()}
|
|
4606
5543
|
`;
|
|
4607
|
-
|
|
4608
|
-
upsertOverviewRow(featurePlanDir, story.feature, nn,
|
|
5544
|
+
fs28.writeFileSync(targetPath, body, "utf8");
|
|
5545
|
+
upsertOverviewRow(featurePlanDir, story.feature, nn, path20.basename(targetPath), story, true);
|
|
4609
5546
|
return { planFile: targetPath, sequenceNumber: nn, overwrote };
|
|
4610
5547
|
}
|
|
4611
5548
|
function findExistingPlan(featureDir, storyId) {
|
|
4612
|
-
if (!
|
|
5549
|
+
if (!fs28.existsSync(featureDir)) return void 0;
|
|
4613
5550
|
const pattern = /^(\d{2,})-story-.+\.md$/;
|
|
4614
|
-
for (const entry of
|
|
5551
|
+
for (const entry of fs28.readdirSync(featureDir)) {
|
|
4615
5552
|
if (entry.endsWith(".partial.md")) continue;
|
|
4616
5553
|
const m = entry.match(pattern);
|
|
4617
5554
|
if (!m) continue;
|
|
4618
|
-
if (entry.includes(storyId)) return { absPath:
|
|
5555
|
+
if (entry.includes(storyId)) return { absPath: path20.join(featureDir, entry), nn: parseInt(m[1], 10) };
|
|
4619
5556
|
}
|
|
4620
5557
|
return void 0;
|
|
4621
5558
|
}
|
|
4622
5559
|
function findExistingPartialPlan(featureDir, storyId) {
|
|
4623
|
-
if (!
|
|
5560
|
+
if (!fs28.existsSync(featureDir)) return void 0;
|
|
4624
5561
|
const pattern = /^(\d{2,})-story-.+\.partial\.md$/;
|
|
4625
|
-
for (const entry of
|
|
5562
|
+
for (const entry of fs28.readdirSync(featureDir)) {
|
|
4626
5563
|
const m = entry.match(pattern);
|
|
4627
5564
|
if (!m) continue;
|
|
4628
|
-
if (entry.includes(storyId)) return { absPath:
|
|
5565
|
+
if (entry.includes(storyId)) return { absPath: path20.join(featureDir, entry), nn: parseInt(m[1], 10) };
|
|
4629
5566
|
}
|
|
4630
5567
|
return void 0;
|
|
4631
5568
|
}
|
|
4632
5569
|
function upsertOverviewRow(featureDir, feature, nn, filename, story, isPartial) {
|
|
4633
|
-
const overviewPath =
|
|
5570
|
+
const overviewPath = path20.join(featureDir, "00-overview.md");
|
|
4634
5571
|
const titleCell = isPartial ? `${story.titleHint ?? story.id} (partial)` : story.titleHint ?? story.id;
|
|
4635
5572
|
const row = `| ${formatSequence(nn)} | \`${filename}\` | ${titleCell} | ${story.id} | \u2014 |`;
|
|
4636
|
-
if (!
|
|
5573
|
+
if (!fs28.existsSync(overviewPath)) {
|
|
4637
5574
|
const content = `# ${feature} \u2014 plan overview
|
|
4638
5575
|
|
|
4639
5576
|
Entry point for the **${feature}** feature. Stories execute in order by their \`NN\` prefix.
|
|
@@ -4644,10 +5581,10 @@ Entry point for the **${feature}** feature. Stories execute in order by their \`
|
|
|
4644
5581
|
|----|------|-------|------------|------------|
|
|
4645
5582
|
${row}
|
|
4646
5583
|
`;
|
|
4647
|
-
|
|
5584
|
+
fs28.writeFileSync(overviewPath, content, "utf8");
|
|
4648
5585
|
return;
|
|
4649
5586
|
}
|
|
4650
|
-
const current =
|
|
5587
|
+
const current = fs28.readFileSync(overviewPath, "utf8");
|
|
4651
5588
|
const lines = current.split("\n");
|
|
4652
5589
|
const existingIdx = lines.findIndex((l) => l.includes(`\`${filename}\``) || l.includes(`| ${formatSequence(nn)} |`));
|
|
4653
5590
|
if (existingIdx >= 0) {
|
|
@@ -4656,7 +5593,7 @@ ${row}
|
|
|
4656
5593
|
const tableEnd = findTableEnd(lines);
|
|
4657
5594
|
lines.splice(tableEnd, 0, row);
|
|
4658
5595
|
}
|
|
4659
|
-
|
|
5596
|
+
fs28.writeFileSync(overviewPath, lines.join("\n"), "utf8");
|
|
4660
5597
|
}
|
|
4661
5598
|
function findTableEnd(lines) {
|
|
4662
5599
|
let inTable = false;
|
|
@@ -4675,9 +5612,17 @@ function buildMetadataHeader(args) {
|
|
|
4675
5612
|
`generated by ${args.provider}/${args.model}`,
|
|
4676
5613
|
`at ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
4677
5614
|
`${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
|
|
5615
|
+
`${args.inputTokens} in / ${args.outputTokens} out tokens`
|
|
4680
5616
|
].filter(Boolean);
|
|
5617
|
+
if (args.scoutEnabled !== void 0) {
|
|
5618
|
+
parts.push(`scout=${args.scoutEnabled ? "on" : "off"}`);
|
|
5619
|
+
}
|
|
5620
|
+
if (args.validationEnabled !== void 0) {
|
|
5621
|
+
parts.push(`validation=${args.validationEnabled ? "on" : "off"}`);
|
|
5622
|
+
}
|
|
5623
|
+
if (args.validationIssueCount !== void 0) {
|
|
5624
|
+
parts.push(`issues=${args.validationIssueCount}`);
|
|
5625
|
+
}
|
|
4681
5626
|
const line1 = `<!-- squad-kit: ${parts.join(", ")} -->`;
|
|
4682
5627
|
if (args.planStatus === "partial") {
|
|
4683
5628
|
return `${line1}
|
|
@@ -4701,16 +5646,16 @@ function buildCopyPlanPromptMarkdown(config2, intakeContent) {
|
|
|
4701
5646
|
}
|
|
4702
5647
|
|
|
4703
5648
|
// src/core/last-run.ts
|
|
4704
|
-
import
|
|
4705
|
-
import
|
|
5649
|
+
import fs29 from "fs/promises";
|
|
5650
|
+
import path21 from "path";
|
|
4706
5651
|
async function writeLastRun(paths, rec) {
|
|
4707
|
-
const file =
|
|
4708
|
-
await
|
|
5652
|
+
const file = path21.join(paths.squadDir, ".last-run.json");
|
|
5653
|
+
await fs29.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
|
|
4709
5654
|
}
|
|
4710
5655
|
async function readLastRun(paths) {
|
|
4711
|
-
const file =
|
|
5656
|
+
const file = path21.join(paths.squadDir, ".last-run.json");
|
|
4712
5657
|
try {
|
|
4713
|
-
const raw = await
|
|
5658
|
+
const raw = await fs29.readFile(file, "utf8");
|
|
4714
5659
|
const parsed = JSON.parse(raw);
|
|
4715
5660
|
if (parsed.version !== 1) return void 0;
|
|
4716
5661
|
return parsed;
|
|
@@ -4739,6 +5684,59 @@ function formatPlannerCacheLine(opts) {
|
|
|
4739
5684
|
return "cache miss 0% \u2014 run `squad doctor` to diagnose";
|
|
4740
5685
|
}
|
|
4741
5686
|
|
|
5687
|
+
// src/core/plans-index.ts
|
|
5688
|
+
import fs30 from "fs";
|
|
5689
|
+
import path22 from "path";
|
|
5690
|
+
var MAX_ENTRIES = 30;
|
|
5691
|
+
function firstMarkdownH1(src) {
|
|
5692
|
+
const lines = src.split(/\r?\n/);
|
|
5693
|
+
for (const line2 of lines) {
|
|
5694
|
+
const m = /^\s*#\s+(.+?)\s*$/.exec(line2);
|
|
5695
|
+
if (m) return m[1].trim();
|
|
5696
|
+
}
|
|
5697
|
+
return void 0;
|
|
5698
|
+
}
|
|
5699
|
+
function collectPlanMarkdownFiles(plansDir) {
|
|
5700
|
+
const out = [];
|
|
5701
|
+
const walk2 = (dir) => {
|
|
5702
|
+
let ents;
|
|
5703
|
+
try {
|
|
5704
|
+
ents = fs30.readdirSync(dir, { withFileTypes: true });
|
|
5705
|
+
} catch {
|
|
5706
|
+
return;
|
|
5707
|
+
}
|
|
5708
|
+
for (const ent of ents) {
|
|
5709
|
+
const abs = path22.join(dir, ent.name);
|
|
5710
|
+
if (ent.isDirectory()) {
|
|
5711
|
+
walk2(abs);
|
|
5712
|
+
} else if (ent.isFile() && ent.name.endsWith(".md")) {
|
|
5713
|
+
if (ent.name === "00-index.md" || ent.name === "00-overview.md") continue;
|
|
5714
|
+
out.push(abs);
|
|
5715
|
+
}
|
|
5716
|
+
}
|
|
5717
|
+
};
|
|
5718
|
+
if (fs30.existsSync(plansDir)) walk2(plansDir);
|
|
5719
|
+
out.sort((a, b) => a.localeCompare(b));
|
|
5720
|
+
return out.slice(0, MAX_ENTRIES);
|
|
5721
|
+
}
|
|
5722
|
+
function buildPlansIndex(paths) {
|
|
5723
|
+
const files = collectPlanMarkdownFiles(paths.plansDir);
|
|
5724
|
+
if (files.length === 0) return "(no prior plans yet)";
|
|
5725
|
+
const bullets = [];
|
|
5726
|
+
for (const abs of files) {
|
|
5727
|
+
const rel = path22.relative(paths.root, abs).split(path22.sep).join("/");
|
|
5728
|
+
let title = "";
|
|
5729
|
+
try {
|
|
5730
|
+
const raw = fs30.readFileSync(abs, "utf8");
|
|
5731
|
+
title = firstMarkdownH1(raw) ?? path22.basename(abs);
|
|
5732
|
+
} catch {
|
|
5733
|
+
title = path22.basename(abs);
|
|
5734
|
+
}
|
|
5735
|
+
bullets.push(`- ${rel} \u2014 "${title}"`);
|
|
5736
|
+
}
|
|
5737
|
+
return bullets.join("\n");
|
|
5738
|
+
}
|
|
5739
|
+
|
|
4742
5740
|
// src/commands/new-plan.ts
|
|
4743
5741
|
function decideMode(opts, config2) {
|
|
4744
5742
|
if (opts.api && opts.copy) {
|
|
@@ -4770,8 +5768,8 @@ async function runNewPlan(intakePath, opts) {
|
|
|
4770
5768
|
}
|
|
4771
5769
|
}
|
|
4772
5770
|
function resolveFromPath(intakePath, stories, root) {
|
|
4773
|
-
const resolved =
|
|
4774
|
-
if (!
|
|
5771
|
+
const resolved = path23.resolve(intakePath.trim());
|
|
5772
|
+
if (!fs31.existsSync(resolved)) {
|
|
4775
5773
|
throw new Error(
|
|
4776
5774
|
`Intake not found: ${resolved}. Run \`squad list\` to see intakes, or \`squad new-story\` to create one first.`
|
|
4777
5775
|
);
|
|
@@ -4783,7 +5781,7 @@ function resolveFromPath(intakePath, stories, root) {
|
|
|
4783
5781
|
);
|
|
4784
5782
|
}
|
|
4785
5783
|
function relativeStoriesDir(root) {
|
|
4786
|
-
return
|
|
5784
|
+
return path23.relative(root, path23.join(root, ".squad", "stories"));
|
|
4787
5785
|
}
|
|
4788
5786
|
async function pickStory(stories, opts) {
|
|
4789
5787
|
const candidates = opts.all ? stories : stories.filter((s) => !s.planFile);
|
|
@@ -4814,7 +5812,7 @@ async function pickStory(stories, opts) {
|
|
|
4814
5812
|
value: s.intakePath
|
|
4815
5813
|
}))
|
|
4816
5814
|
});
|
|
4817
|
-
return candidates.find((s) =>
|
|
5815
|
+
return candidates.find((s) => path23.resolve(s.intakePath) === path23.resolve(pick));
|
|
4818
5816
|
}
|
|
4819
5817
|
function formatLabel(s) {
|
|
4820
5818
|
const head = `${s.feature} / ${s.id}`;
|
|
@@ -4860,13 +5858,13 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4860
5858
|
`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
5859
|
);
|
|
4862
5860
|
}
|
|
4863
|
-
const
|
|
4864
|
-
const
|
|
5861
|
+
const modelId = modelFor(planner.provider, "plan", planner.modelOverride);
|
|
5862
|
+
const { model } = resolveModel(planner.provider, modelId, apiKey);
|
|
4865
5863
|
const budget = new Budget(planner.budget);
|
|
4866
5864
|
banner();
|
|
4867
5865
|
step(`planning ${story.feature} / ${story.id}`);
|
|
4868
5866
|
kv("provider", planner.provider);
|
|
4869
|
-
kv("model",
|
|
5867
|
+
kv("model", modelId);
|
|
4870
5868
|
kv(
|
|
4871
5869
|
"budget",
|
|
4872
5870
|
`${planner.budget.maxFileReads} reads \xB7 ${(planner.budget.maxContextBytes / 1024).toFixed(0)} KB \xB7 ${planner.budget.maxDurationSeconds}s`
|
|
@@ -4877,7 +5875,7 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4877
5875
|
printPlannerApiCostNotice();
|
|
4878
5876
|
}
|
|
4879
5877
|
const mapSpinner = spinner("building repo map\u2026");
|
|
4880
|
-
const repoMap = buildRepoMap(paths.root);
|
|
5878
|
+
const repoMap = buildRepoMap(paths.root, { format: "tree" });
|
|
4881
5879
|
mapSpinner.succeed(
|
|
4882
5880
|
`repo map ready (${repoMap.split("\n").length - 1} paths \xB7 ${(repoMap.length / 1024).toFixed(1)} KB)`
|
|
4883
5881
|
);
|
|
@@ -4889,6 +5887,29 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4889
5887
|
repoMap
|
|
4890
5888
|
});
|
|
4891
5889
|
const userPrompt = composeUserPrompt({ intakeContent: readFile(story.intakePath) });
|
|
5890
|
+
const scoutEnabled = !opts.noScout && planner.stages?.scout?.enabled !== false;
|
|
5891
|
+
const validationEnabled = !opts.noValidation && planner.validation?.enabled !== false;
|
|
5892
|
+
const strictValidation = !!opts.strictValidation || planner.validation?.strict === true;
|
|
5893
|
+
const maxScoutFiles = opts.maxScoutFiles ?? planner.stages?.scout?.maxFiles ?? 12;
|
|
5894
|
+
let scoutModelId = "";
|
|
5895
|
+
let scoutModelHandle;
|
|
5896
|
+
let scoutSystemPrompt;
|
|
5897
|
+
if (scoutEnabled) {
|
|
5898
|
+
scoutModelId = modelFor(
|
|
5899
|
+
planner.provider,
|
|
5900
|
+
"scout",
|
|
5901
|
+
planner.modelOverride,
|
|
5902
|
+
opts.scoutModel ?? planner.stages?.scout?.modelOverride
|
|
5903
|
+
);
|
|
5904
|
+
scoutModelHandle = resolveModel(planner.provider, scoutModelId, apiKey).model;
|
|
5905
|
+
scoutSystemPrompt = composeScoutSystemPrompt({
|
|
5906
|
+
projectRoots: config2.project.projectRoots ?? ["."],
|
|
5907
|
+
primaryLanguage: config2.project.primaryLanguage ?? "",
|
|
5908
|
+
trackerType: config2.tracker.type,
|
|
5909
|
+
repoMap,
|
|
5910
|
+
plansIndex: buildPlansIndex(paths)
|
|
5911
|
+
});
|
|
5912
|
+
}
|
|
4892
5913
|
const sessionSpinner = { current: null };
|
|
4893
5914
|
const startedAt = Date.now();
|
|
4894
5915
|
const cacheEnabled = planner.cache?.enabled ?? true;
|
|
@@ -4897,11 +5918,43 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4897
5918
|
const unsubscribe = bus.subscribe((e) => {
|
|
4898
5919
|
switch (e.kind) {
|
|
4899
5920
|
case "tool_call": {
|
|
4900
|
-
const
|
|
5921
|
+
const name = e.toolCall.name;
|
|
5922
|
+
const inp = e.toolCall.input;
|
|
5923
|
+
let label;
|
|
5924
|
+
if (name === "read_file") {
|
|
5925
|
+
label = `read ${String(inp.path ?? "<unknown>")}`;
|
|
5926
|
+
} else if (name === "grep") {
|
|
5927
|
+
label = `grep ${String(inp.pattern ?? "?").slice(0, 48)}`;
|
|
5928
|
+
} else if (name === "list_dir") {
|
|
5929
|
+
label = `list_dir ${String(inp.path ?? "?")}`;
|
|
5930
|
+
} else {
|
|
5931
|
+
label = name;
|
|
5932
|
+
}
|
|
4901
5933
|
sessionSpinner.current?.succeed(
|
|
4902
|
-
|
|
5934
|
+
`${label} (${(e.bytesLoaded / 1024).toFixed(1)} KB \xB7 ${(e.totalBytes / 1024).toFixed(1)} KB / ${(planner.budget.maxContextBytes / 1024).toFixed(0)} KB)`
|
|
4903
5935
|
);
|
|
4904
|
-
sessionSpinner.current = spinner("
|
|
5936
|
+
sessionSpinner.current = spinner("next tool\u2026");
|
|
5937
|
+
break;
|
|
5938
|
+
}
|
|
5939
|
+
case "stage_started": {
|
|
5940
|
+
sessionSpinner.current?.stop();
|
|
5941
|
+
if (e.stage === "scout") sessionSpinner.current = spinner("scouting\u2026");
|
|
5942
|
+
else if (e.stage === "draft") sessionSpinner.current = spinner("drafting\u2026");
|
|
5943
|
+
else if (e.stage === "validation") sessionSpinner.current = spinner("validating\u2026");
|
|
5944
|
+
break;
|
|
5945
|
+
}
|
|
5946
|
+
case "stage_complete": {
|
|
5947
|
+
if (!e.success && e.errorMessage && e.stage === "scout") {
|
|
5948
|
+
sessionSpinner.current?.stop();
|
|
5949
|
+
warning(`scout stage failed: ${e.errorMessage}`);
|
|
5950
|
+
}
|
|
5951
|
+
break;
|
|
5952
|
+
}
|
|
5953
|
+
case "scout_result": {
|
|
5954
|
+
const preview = e.selected.slice(0, 4).join(", ");
|
|
5955
|
+
const more = e.selected.length > 4 ? "\u2026" : "";
|
|
5956
|
+
sessionSpinner.current?.succeed(`scout picked ${e.selected.length} files: ${preview}${more}`);
|
|
5957
|
+
sessionSpinner.current = spinner("drafting\u2026");
|
|
4905
5958
|
break;
|
|
4906
5959
|
}
|
|
4907
5960
|
case "assistant_text":
|
|
@@ -4937,9 +5990,9 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4937
5990
|
try {
|
|
4938
5991
|
result = await runPlanner({
|
|
4939
5992
|
root: paths.root,
|
|
4940
|
-
provider,
|
|
4941
5993
|
model,
|
|
4942
|
-
|
|
5994
|
+
provider: planner.provider,
|
|
5995
|
+
modelId,
|
|
4943
5996
|
systemPrompt,
|
|
4944
5997
|
userPrompt,
|
|
4945
5998
|
budget,
|
|
@@ -4947,7 +6000,19 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4947
6000
|
cacheEnabled,
|
|
4948
6001
|
decideOnLimit,
|
|
4949
6002
|
events: bus,
|
|
4950
|
-
runId
|
|
6003
|
+
runId,
|
|
6004
|
+
stages: {
|
|
6005
|
+
scout: {
|
|
6006
|
+
enabled: scoutEnabled,
|
|
6007
|
+
model: scoutModelHandle,
|
|
6008
|
+
modelId: scoutModelId,
|
|
6009
|
+
maxFiles: maxScoutFiles,
|
|
6010
|
+
maxOutputTokens: planner.stages?.scout?.maxOutputTokens ?? 2048
|
|
6011
|
+
}
|
|
6012
|
+
},
|
|
6013
|
+
validation: { enabled: validationEnabled, strict: strictValidation },
|
|
6014
|
+
toolsEnabled: planner.tools,
|
|
6015
|
+
scoutSystemPrompt
|
|
4951
6016
|
});
|
|
4952
6017
|
} finally {
|
|
4953
6018
|
unsubscribe();
|
|
@@ -4960,17 +6025,22 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4960
6025
|
}
|
|
4961
6026
|
const snap = budget.snapshot();
|
|
4962
6027
|
const elapsedMs = Date.now() - startedAt;
|
|
4963
|
-
const
|
|
6028
|
+
const issueCount = result.validation?.issues.length ?? 0;
|
|
6029
|
+
const issuesByKind = summariseIssuesByKind(result.validation?.issues ?? []);
|
|
6030
|
+
const validationBlocks = strictValidation && validationEnabled && issueCount > 0;
|
|
6031
|
+
const success2 = result.finishedNormally && !result.timedOut && !result.userCancelled && !validationBlocks;
|
|
4964
6032
|
const header = buildMetadataHeader({
|
|
4965
6033
|
provider: planner.provider,
|
|
4966
|
-
model,
|
|
6034
|
+
model: modelId,
|
|
4967
6035
|
reads: snap.reads,
|
|
4968
6036
|
bytes: snap.bytes,
|
|
4969
6037
|
inputTokens: snap.usage.inputTokens,
|
|
4970
6038
|
outputTokens: snap.usage.outputTokens,
|
|
4971
6039
|
durationMs: elapsedMs,
|
|
4972
|
-
|
|
4973
|
-
|
|
6040
|
+
planStatus: success2 ? void 0 : "partial",
|
|
6041
|
+
scoutEnabled,
|
|
6042
|
+
validationEnabled,
|
|
6043
|
+
validationIssueCount: issueCount
|
|
4974
6044
|
});
|
|
4975
6045
|
const { planFile, sequenceNumber, overwrote } = writePlanFile({
|
|
4976
6046
|
paths,
|
|
@@ -4980,18 +6050,18 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4980
6050
|
metadataHeader: header,
|
|
4981
6051
|
partial: !success2
|
|
4982
6052
|
});
|
|
4983
|
-
const relPlan =
|
|
6053
|
+
const relPlan = path23.relative(paths.root, planFile);
|
|
4984
6054
|
try {
|
|
4985
6055
|
await writeLastRun(paths, {
|
|
4986
6056
|
stats: result.stats,
|
|
4987
6057
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4988
6058
|
provider: planner.provider,
|
|
4989
|
-
model
|
|
6059
|
+
model: modelId
|
|
4990
6060
|
});
|
|
4991
6061
|
await appendRun(paths, {
|
|
4992
6062
|
runId,
|
|
4993
6063
|
provider: planner.provider,
|
|
4994
|
-
model,
|
|
6064
|
+
model: modelId,
|
|
4995
6065
|
feature: story.feature,
|
|
4996
6066
|
storyId: story.id,
|
|
4997
6067
|
startedAt: new Date(startedAt).toISOString(),
|
|
@@ -5001,7 +6071,19 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
5001
6071
|
planFile: relPlan,
|
|
5002
6072
|
stats: result.stats,
|
|
5003
6073
|
cacheEnabled,
|
|
5004
|
-
durationMs: elapsedMs
|
|
6074
|
+
durationMs: elapsedMs,
|
|
6075
|
+
scout: {
|
|
6076
|
+
enabled: scoutEnabled,
|
|
6077
|
+
selectedCount: result.scout?.selected.length,
|
|
6078
|
+
tokensUsed: result.scout?.tokensUsed,
|
|
6079
|
+
durationMs: result.scout?.durationMs
|
|
6080
|
+
},
|
|
6081
|
+
validation: {
|
|
6082
|
+
enabled: validationEnabled,
|
|
6083
|
+
issuesCount: issueCount,
|
|
6084
|
+
issuesByKind,
|
|
6085
|
+
durationMs: result.validation?.durationMs
|
|
6086
|
+
}
|
|
5005
6087
|
});
|
|
5006
6088
|
} catch {
|
|
5007
6089
|
}
|
|
@@ -5009,11 +6091,19 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
5009
6091
|
summaryBox(success2 ? " plan generated " : " plan saved (incomplete) ", [
|
|
5010
6092
|
{ key: "file", value: relPlan },
|
|
5011
6093
|
{ key: "nn", value: String(sequenceNumber).padStart(2, "0") },
|
|
5012
|
-
{ key: "model", value: `${planner.provider}/${
|
|
6094
|
+
{ key: "model", value: `${planner.provider}/${modelId}` },
|
|
6095
|
+
{
|
|
6096
|
+
key: "scout",
|
|
6097
|
+
value: scoutEnabled ? `${result.scout?.selected.length ?? 0} files \xB7 ${result.scout?.tokensUsed ?? 0} tok` : "disabled"
|
|
6098
|
+
},
|
|
6099
|
+
{
|
|
6100
|
+
key: "validation",
|
|
6101
|
+
value: validationEnabled ? `${issueCount} issue(s)` : "disabled"
|
|
6102
|
+
},
|
|
5013
6103
|
{ key: "reads", value: `${snap.reads} files \xB7 ${(snap.bytes / 1024).toFixed(1)} KB` },
|
|
5014
6104
|
{
|
|
5015
6105
|
key: "tokens",
|
|
5016
|
-
value: `${snap.usage.inputTokens} in \xB7 ${snap.usage.outputTokens} out
|
|
6106
|
+
value: `${snap.usage.inputTokens} in \xB7 ${snap.usage.outputTokens} out`
|
|
5017
6107
|
},
|
|
5018
6108
|
{ key: "cache", value: formatPlannerCacheLine({ cacheEnabled, stats: result.stats }) },
|
|
5019
6109
|
{
|
|
@@ -5023,25 +6113,45 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
5023
6113
|
{ key: "time", value: `${Math.round(elapsedMs / 1e3)}s` },
|
|
5024
6114
|
{
|
|
5025
6115
|
key: "action",
|
|
5026
|
-
value: success2 ? overwrote ? "overwrote existing plan" : "new plan" : "partial plan (review limits)"
|
|
6116
|
+
value: success2 ? overwrote ? "overwrote existing plan" : "new plan" : validationBlocks ? "partial plan (validation issues \u2014 review or use --no-validation)" : "partial plan (review limits)"
|
|
5027
6117
|
}
|
|
5028
6118
|
]);
|
|
6119
|
+
if (issueCount > 0 && validationEnabled) {
|
|
6120
|
+
blank();
|
|
6121
|
+
warning(`Validation reported ${issueCount} issue(s) (heuristic; first 10 shown):`);
|
|
6122
|
+
for (const iss of result.validation.issues.slice(0, 10)) {
|
|
6123
|
+
info(
|
|
6124
|
+
` [${iss.severity}] ${iss.kind}${iss.path ? ` ${iss.path}` : ""} \u2014 ${iss.detail}`
|
|
6125
|
+
);
|
|
6126
|
+
}
|
|
6127
|
+
if (issueCount > 10) {
|
|
6128
|
+
info(` (+${issueCount - 10} more \u2014 see metadata header issues=\u2026)`);
|
|
6129
|
+
}
|
|
6130
|
+
if (issueCount > 50) {
|
|
6131
|
+
warning("Large issue count may mean the validator is misfiring; try `squad new-plan --no-validation` to compare.");
|
|
6132
|
+
}
|
|
6133
|
+
}
|
|
5029
6134
|
if (!success2) {
|
|
5030
|
-
|
|
6135
|
+
if (!validationBlocks || !result.finishedNormally) {
|
|
6136
|
+
printPlannerRunIncompleteSummary(result, relPlan);
|
|
6137
|
+
} else {
|
|
6138
|
+
blank();
|
|
6139
|
+
warning("Plan saved as partial because strict validation reported issues.");
|
|
6140
|
+
}
|
|
5031
6141
|
throw new SquadExit(2);
|
|
5032
6142
|
}
|
|
5033
6143
|
if (result.budgetExhausted) {
|
|
5034
6144
|
info("Note: file-read budget was reached; the model finalised without further reads.");
|
|
5035
6145
|
}
|
|
5036
6146
|
blank();
|
|
5037
|
-
info(`next \u2192 open a new agent chat and attach only ${
|
|
6147
|
+
info(`next \u2192 open a new agent chat and attach only ${path23.basename(planFile)}`);
|
|
5038
6148
|
}
|
|
5039
6149
|
async function emitCopyPrompt(story, paths, config2, clipboard) {
|
|
5040
6150
|
const intakeContent = readFile(story.intakePath);
|
|
5041
6151
|
const composed = buildCopyPlanPromptMarkdown(config2, intakeContent);
|
|
5042
|
-
const promptFile =
|
|
5043
|
-
|
|
5044
|
-
const relPromptFile =
|
|
6152
|
+
const promptFile = path23.join(paths.squadDir, ".last-copy-prompt.md");
|
|
6153
|
+
fs31.writeFileSync(promptFile, composed, "utf8");
|
|
6154
|
+
const relPromptFile = path23.relative(paths.root, promptFile);
|
|
5045
6155
|
const bytes = Buffer.byteLength(composed, "utf8");
|
|
5046
6156
|
const estTokens = Math.round(bytes / 4);
|
|
5047
6157
|
const clipResult = clipboard ? await copyToClipboard(composed) : { ok: false, reason: "clipboard disabled (--no-clipboard)" };
|
|
@@ -5099,17 +6209,17 @@ function printModelBanner(config2) {
|
|
|
5099
6209
|
}
|
|
5100
6210
|
|
|
5101
6211
|
// src/commands/status.ts
|
|
5102
|
-
import
|
|
6212
|
+
import fs32 from "fs";
|
|
5103
6213
|
var kw = 28;
|
|
5104
6214
|
function probeTrackerCredential(config2, overlay) {
|
|
5105
|
-
const { client, error
|
|
6215
|
+
const { client, error } = clientFor(config2, overlay);
|
|
5106
6216
|
if (client) {
|
|
5107
6217
|
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
6218
|
return { state: "present", detail: fromEnv ? "env" : "secrets.yaml" };
|
|
5109
6219
|
}
|
|
5110
6220
|
return {
|
|
5111
6221
|
state: "missing",
|
|
5112
|
-
hint:
|
|
6222
|
+
hint: error?.detail ?? "Run `squad init` to enter credentials."
|
|
5113
6223
|
};
|
|
5114
6224
|
}
|
|
5115
6225
|
function formatCredentialStatus(s) {
|
|
@@ -5125,7 +6235,7 @@ async function runStatus() {
|
|
|
5125
6235
|
const scan = scanPlans(paths.plansDir);
|
|
5126
6236
|
const storyCount = listStories(paths).length;
|
|
5127
6237
|
const planCount = scan.usedNumbers.length;
|
|
5128
|
-
const secrets =
|
|
6238
|
+
const secrets = fs32.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
5129
6239
|
const overlay = overlayTrackerEnv(secrets);
|
|
5130
6240
|
banner();
|
|
5131
6241
|
step("squad-kit status");
|
|
@@ -5179,13 +6289,13 @@ function buildTrackerLine(config2, overlay) {
|
|
|
5179
6289
|
}
|
|
5180
6290
|
|
|
5181
6291
|
// src/commands/list.ts
|
|
5182
|
-
import
|
|
6292
|
+
import path24 from "path";
|
|
5183
6293
|
async function runList(opts) {
|
|
5184
6294
|
const root = requireSquadRoot();
|
|
5185
6295
|
const paths = buildPaths(root);
|
|
5186
6296
|
const records = listStories(paths, { feature: opts.feature });
|
|
5187
6297
|
const rows = records.map((r) => {
|
|
5188
|
-
const planAbs = r.planFile ?
|
|
6298
|
+
const planAbs = r.planFile ? path24.join(paths.plansDir, r.feature, r.planFile) : void 0;
|
|
5189
6299
|
const meta = planAbs ? readPlanMetadata(planAbs) : void 0;
|
|
5190
6300
|
return {
|
|
5191
6301
|
feature: r.feature,
|
|
@@ -5224,8 +6334,8 @@ function stripAnsi(s) {
|
|
|
5224
6334
|
}
|
|
5225
6335
|
|
|
5226
6336
|
// src/commands/tracker-link.ts
|
|
5227
|
-
import
|
|
5228
|
-
import
|
|
6337
|
+
import fs33 from "fs";
|
|
6338
|
+
import path25 from "path";
|
|
5229
6339
|
import { input as input4, select as select7 } from "@inquirer/prompts";
|
|
5230
6340
|
async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
5231
6341
|
const root = requireSquadRoot();
|
|
@@ -5272,17 +6382,17 @@ async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
|
5272
6382
|
`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
6383
|
);
|
|
5274
6384
|
}
|
|
5275
|
-
const resolved =
|
|
5276
|
-
const intakeFile = resolved.endsWith("intake.md") ? resolved :
|
|
5277
|
-
if (!
|
|
6385
|
+
const resolved = path25.resolve(resolvedStoryPath);
|
|
6386
|
+
const intakeFile = resolved.endsWith("intake.md") ? resolved : path25.join(resolved, "intake.md");
|
|
6387
|
+
if (!fs33.existsSync(intakeFile)) {
|
|
5278
6388
|
throw new Error(
|
|
5279
6389
|
`intake.md not found at ${intakeFile}. Run \`squad list\` to see valid paths, or \`squad new-story\` to create a story first.`
|
|
5280
6390
|
);
|
|
5281
6391
|
}
|
|
5282
|
-
const content =
|
|
6392
|
+
const content = fs33.readFileSync(intakeFile, "utf8");
|
|
5283
6393
|
const updated = upsertTrackerId(content, resolvedTrackerId);
|
|
5284
|
-
|
|
5285
|
-
success(`Linked tracker id ${resolvedTrackerId} \u2192 ${
|
|
6394
|
+
fs33.writeFileSync(intakeFile, updated, "utf8");
|
|
6395
|
+
success(`Linked tracker id ${resolvedTrackerId} \u2192 ${path25.relative(root, intakeFile)}`);
|
|
5286
6396
|
blank();
|
|
5287
6397
|
step("Next:");
|
|
5288
6398
|
info("1) Review the intake \u2014 the Source block at the top now carries the tracker id.");
|
|
@@ -5300,39 +6410,39 @@ function upsertTrackerId(content, id) {
|
|
|
5300
6410
|
}
|
|
5301
6411
|
|
|
5302
6412
|
// src/commands/doctor-engine.ts
|
|
5303
|
-
import
|
|
5304
|
-
import
|
|
6413
|
+
import fs34 from "fs";
|
|
6414
|
+
import path26 from "path";
|
|
5305
6415
|
async function gatherContext(paths) {
|
|
5306
6416
|
const ctx = { hasLegacyPromptsDir: false };
|
|
5307
6417
|
try {
|
|
5308
6418
|
ctx.config = loadConfig(paths.configFile);
|
|
5309
|
-
} catch (
|
|
5310
|
-
ctx.configError =
|
|
6419
|
+
} catch (err2) {
|
|
6420
|
+
ctx.configError = err2;
|
|
5311
6421
|
}
|
|
5312
6422
|
try {
|
|
5313
|
-
ctx.secrets =
|
|
5314
|
-
} catch (
|
|
5315
|
-
ctx.secretsError =
|
|
6423
|
+
ctx.secrets = fs34.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
6424
|
+
} catch (err2) {
|
|
6425
|
+
ctx.secretsError = err2;
|
|
5316
6426
|
}
|
|
5317
|
-
ctx.hasLegacyPromptsDir =
|
|
6427
|
+
ctx.hasLegacyPromptsDir = fs34.existsSync(paths.promptsDir);
|
|
5318
6428
|
return ctx;
|
|
5319
6429
|
}
|
|
5320
6430
|
function gitignoreHasManagedBlock(repoRoot) {
|
|
5321
|
-
const gitignore =
|
|
5322
|
-
return
|
|
6431
|
+
const gitignore = path26.join(repoRoot, ".gitignore");
|
|
6432
|
+
return fs34.existsSync(gitignore) && fs34.readFileSync(gitignore, "utf8").includes(".squad/secrets.yaml");
|
|
5323
6433
|
}
|
|
5324
6434
|
function gitignoreHasTrashPattern(repoRoot) {
|
|
5325
|
-
const gitignore =
|
|
5326
|
-
return
|
|
6435
|
+
const gitignore = path26.join(repoRoot, ".gitignore");
|
|
6436
|
+
return fs34.existsSync(gitignore) && fs34.readFileSync(gitignore, "utf8").includes(SQUAD_TRASH_PATTERN);
|
|
5327
6437
|
}
|
|
5328
6438
|
async function checkDirStructure(paths, _ctx, fix) {
|
|
5329
|
-
const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !
|
|
6439
|
+
const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !fs34.existsSync(p));
|
|
5330
6440
|
if (need.length === 0) {
|
|
5331
6441
|
return { id: "dirs", name: ".squad/ directory structure", status: "ok" };
|
|
5332
6442
|
}
|
|
5333
6443
|
if (fix) {
|
|
5334
6444
|
for (const p of need) {
|
|
5335
|
-
|
|
6445
|
+
fs34.mkdirSync(p, { recursive: true });
|
|
5336
6446
|
}
|
|
5337
6447
|
return { id: "dirs", name: ".squad/ directory structure", status: "ok", detail: "repaired" };
|
|
5338
6448
|
}
|
|
@@ -5340,7 +6450,7 @@ async function checkDirStructure(paths, _ctx, fix) {
|
|
|
5340
6450
|
id: "dirs",
|
|
5341
6451
|
name: ".squad/ directory structure",
|
|
5342
6452
|
status: "warn",
|
|
5343
|
-
detail: `missing: ${need.map((p) =>
|
|
6453
|
+
detail: `missing: ${need.map((p) => path26.relative(paths.root, p)).join(", ")}`,
|
|
5344
6454
|
fixable: true,
|
|
5345
6455
|
fixHint: "squad doctor --fix"
|
|
5346
6456
|
};
|
|
@@ -5395,15 +6505,15 @@ async function checkSecretsPermissions(paths, _ctx, fix) {
|
|
|
5395
6505
|
if (process.platform === "win32") {
|
|
5396
6506
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "skip", detail: "Windows" };
|
|
5397
6507
|
}
|
|
5398
|
-
if (!
|
|
6508
|
+
if (!fs34.existsSync(paths.secretsFile)) {
|
|
5399
6509
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "not present" };
|
|
5400
6510
|
}
|
|
5401
|
-
const mode =
|
|
6511
|
+
const mode = fs34.statSync(paths.secretsFile).mode & 511;
|
|
5402
6512
|
if (mode === 384) {
|
|
5403
6513
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok" };
|
|
5404
6514
|
}
|
|
5405
6515
|
if (fix) {
|
|
5406
|
-
|
|
6516
|
+
fs34.chmodSync(paths.secretsFile, 384);
|
|
5407
6517
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "repaired" };
|
|
5408
6518
|
}
|
|
5409
6519
|
return {
|
|
@@ -5424,7 +6534,7 @@ async function checkSecretsParseable(_paths, ctx) {
|
|
|
5424
6534
|
detail: ctx.secretsError.message
|
|
5425
6535
|
};
|
|
5426
6536
|
}
|
|
5427
|
-
if (!
|
|
6537
|
+
if (!fs34.existsSync(_paths.secretsFile)) {
|
|
5428
6538
|
return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok", detail: "not present" };
|
|
5429
6539
|
}
|
|
5430
6540
|
return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok" };
|
|
@@ -5484,14 +6594,6 @@ async function checkPlannerConfig(_paths, ctx) {
|
|
|
5484
6594
|
detail: "planner budget limits must be > 0"
|
|
5485
6595
|
};
|
|
5486
6596
|
}
|
|
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
6597
|
return { id: "planner-config", name: "planner configuration", status: "ok" };
|
|
5496
6598
|
}
|
|
5497
6599
|
async function checkPlannerCredential(_paths, ctx) {
|
|
@@ -5566,12 +6668,12 @@ async function checkPlannerModel(_paths, ctx) {
|
|
|
5566
6668
|
status: "ok",
|
|
5567
6669
|
detail: `${model} (${provider})`
|
|
5568
6670
|
};
|
|
5569
|
-
} catch (
|
|
6671
|
+
} catch (err2) {
|
|
5570
6672
|
return {
|
|
5571
6673
|
id: "planner-model",
|
|
5572
6674
|
name: "planner model resolves at provider",
|
|
5573
6675
|
status: "warn",
|
|
5574
|
-
detail:
|
|
6676
|
+
detail: err2.message
|
|
5575
6677
|
};
|
|
5576
6678
|
}
|
|
5577
6679
|
}
|
|
@@ -5704,8 +6806,8 @@ async function checkTrackerConfig(_paths, ctx) {
|
|
|
5704
6806
|
}
|
|
5705
6807
|
return { id: "tracker-config", name: "tracker configuration", status: "ok" };
|
|
5706
6808
|
}
|
|
5707
|
-
function formatClientError(
|
|
5708
|
-
return `${
|
|
6809
|
+
function formatClientError(err2) {
|
|
6810
|
+
return `${err2.message} ${err2.detail}`.trim();
|
|
5709
6811
|
}
|
|
5710
6812
|
async function checkTrackerCredential(_paths, ctx) {
|
|
5711
6813
|
if (!ctx.config) {
|
|
@@ -5716,13 +6818,13 @@ async function checkTrackerCredential(_paths, ctx) {
|
|
|
5716
6818
|
}
|
|
5717
6819
|
const secrets = ctx.secrets ?? {};
|
|
5718
6820
|
const overlay = overlayTrackerEnv(secrets);
|
|
5719
|
-
const { client, error
|
|
5720
|
-
if (
|
|
6821
|
+
const { client, error } = clientFor(ctx.config, overlay);
|
|
6822
|
+
if (error) {
|
|
5721
6823
|
return {
|
|
5722
6824
|
id: "tracker-cred",
|
|
5723
6825
|
name: "tracker credential resolves",
|
|
5724
6826
|
status: "fail",
|
|
5725
|
-
detail: formatClientError(
|
|
6827
|
+
detail: formatClientError(error)
|
|
5726
6828
|
};
|
|
5727
6829
|
}
|
|
5728
6830
|
if (!client) {
|
|
@@ -5741,8 +6843,8 @@ async function checkTrackerConnectivity(paths, ctx) {
|
|
|
5741
6843
|
}
|
|
5742
6844
|
const secrets = ctx.secrets ?? {};
|
|
5743
6845
|
const overlay = overlayTrackerEnv(secrets);
|
|
5744
|
-
const { client, error
|
|
5745
|
-
if (
|
|
6846
|
+
const { client, error } = clientFor(ctx.config, overlay);
|
|
6847
|
+
if (error || !client) {
|
|
5746
6848
|
return { id: "tracker-live", name: "tracker connectivity", status: "skip", detail: "no credential" };
|
|
5747
6849
|
}
|
|
5748
6850
|
if (client.name === "jira") {
|
|
@@ -5785,12 +6887,12 @@ async function checkTrackerConnectivity(paths, ctx) {
|
|
|
5785
6887
|
async function guardCheck(checkName, fn) {
|
|
5786
6888
|
try {
|
|
5787
6889
|
return await fn();
|
|
5788
|
-
} catch (
|
|
6890
|
+
} catch (err2) {
|
|
5789
6891
|
return {
|
|
5790
6892
|
id: "unexpected",
|
|
5791
6893
|
name: checkName,
|
|
5792
6894
|
status: "fail",
|
|
5793
|
-
detail:
|
|
6895
|
+
detail: err2.message
|
|
5794
6896
|
};
|
|
5795
6897
|
}
|
|
5796
6898
|
}
|
|
@@ -5918,23 +7020,23 @@ async function runDoctor(opts) {
|
|
|
5918
7020
|
}
|
|
5919
7021
|
|
|
5920
7022
|
// src/commands/migrate.ts
|
|
5921
|
-
import
|
|
5922
|
-
import
|
|
7023
|
+
import fs35 from "fs";
|
|
7024
|
+
import path27 from "path";
|
|
5923
7025
|
import { confirm as confirm7 } from "@inquirer/prompts";
|
|
5924
7026
|
var LEGACY_PROMPTS_MIGRATION = {
|
|
5925
7027
|
id: "legacy-prompts",
|
|
5926
7028
|
name: "Remove legacy .squad/prompts/",
|
|
5927
|
-
isApplied: (paths) => !
|
|
5928
|
-
describe: (paths) => `delete ${
|
|
7029
|
+
isApplied: (paths) => !fs35.existsSync(paths.promptsDir),
|
|
7030
|
+
describe: (paths) => `delete ${path27.relative(paths.root, paths.promptsDir)} (prompts are now bundled in squad-kit)`,
|
|
5929
7031
|
apply: (paths) => {
|
|
5930
7032
|
const target = paths.promptsDir;
|
|
5931
|
-
if (!target.includes(
|
|
7033
|
+
if (!target.includes(path27.sep + ".squad" + path27.sep + "prompts")) {
|
|
5932
7034
|
throw new Error(
|
|
5933
7035
|
`Refusing to delete suspicious path: ${target}. This is a safety check \u2014 run \`squad doctor\` if .squad/ paths look wrong.`
|
|
5934
7036
|
);
|
|
5935
7037
|
}
|
|
5936
|
-
if (
|
|
5937
|
-
|
|
7038
|
+
if (fs35.existsSync(target)) {
|
|
7039
|
+
fs35.rmSync(target, { recursive: true, force: true });
|
|
5938
7040
|
}
|
|
5939
7041
|
}
|
|
5940
7042
|
};
|
|
@@ -5942,8 +7044,8 @@ var GITIGNORE_MIGRATION = {
|
|
|
5942
7044
|
id: "gitignore",
|
|
5943
7045
|
name: "Ensure .gitignore managed block",
|
|
5944
7046
|
isApplied: (paths) => {
|
|
5945
|
-
const f =
|
|
5946
|
-
return
|
|
7047
|
+
const f = path27.join(paths.root, ".gitignore");
|
|
7048
|
+
return fs35.existsSync(f) && fs35.readFileSync(f, "utf8").includes(".squad/secrets.yaml");
|
|
5947
7049
|
},
|
|
5948
7050
|
describe: () => "add squad-kit managed block to .gitignore",
|
|
5949
7051
|
apply: (paths) => {
|
|
@@ -5955,23 +7057,23 @@ var SECRETS_PERMS_MIGRATION = {
|
|
|
5955
7057
|
name: "Tighten .squad/secrets.yaml permissions",
|
|
5956
7058
|
isApplied: (paths) => {
|
|
5957
7059
|
if (process.platform === "win32") return true;
|
|
5958
|
-
if (!
|
|
5959
|
-
const st =
|
|
7060
|
+
if (!fs35.existsSync(paths.secretsFile)) return true;
|
|
7061
|
+
const st = fs35.statSync(paths.secretsFile);
|
|
5960
7062
|
return (st.mode & 511) === 384;
|
|
5961
7063
|
},
|
|
5962
|
-
describe: (paths) => `chmod 0600 ${
|
|
7064
|
+
describe: (paths) => `chmod 0600 ${path27.relative(paths.root, paths.secretsFile)}`,
|
|
5963
7065
|
apply: (paths) => {
|
|
5964
7066
|
if (process.platform === "win32") return;
|
|
5965
|
-
if (
|
|
7067
|
+
if (fs35.existsSync(paths.secretsFile)) fs35.chmodSync(paths.secretsFile, 384);
|
|
5966
7068
|
}
|
|
5967
7069
|
};
|
|
5968
7070
|
var PLANNER_BUDGET_DEFAULTS_MIGRATION = {
|
|
5969
7071
|
id: "planner-budget-defaults",
|
|
5970
7072
|
name: "Backfill planner.budget defaults in config.yaml",
|
|
5971
7073
|
isApplied: (paths) => {
|
|
5972
|
-
if (!
|
|
7074
|
+
if (!fs35.existsSync(paths.configFile)) return true;
|
|
5973
7075
|
try {
|
|
5974
|
-
const raw =
|
|
7076
|
+
const raw = fs35.readFileSync(paths.configFile, "utf8");
|
|
5975
7077
|
if (!/^\s*planner:/m.test(raw)) return true;
|
|
5976
7078
|
if (/^\s*budget:/m.test(raw)) return true;
|
|
5977
7079
|
const cfg = loadConfig(paths.configFile);
|
|
@@ -5990,9 +7092,9 @@ var CONFIG_NORMALIZE_MIGRATION = {
|
|
|
5990
7092
|
id: "config-normalize",
|
|
5991
7093
|
name: "Normalise .squad/config.yaml formatting",
|
|
5992
7094
|
isApplied: (paths) => {
|
|
5993
|
-
if (!
|
|
7095
|
+
if (!fs35.existsSync(paths.configFile)) return true;
|
|
5994
7096
|
try {
|
|
5995
|
-
const raw =
|
|
7097
|
+
const raw = fs35.readFileSync(paths.configFile, "utf8");
|
|
5996
7098
|
const cfg = loadConfig(paths.configFile);
|
|
5997
7099
|
return raw.trim() === serializeConfig(cfg).trim();
|
|
5998
7100
|
} catch {
|
|
@@ -6001,7 +7103,7 @@ var CONFIG_NORMALIZE_MIGRATION = {
|
|
|
6001
7103
|
},
|
|
6002
7104
|
describe: () => "rewrite config.yaml with canonical YAML formatting (drops comments)",
|
|
6003
7105
|
apply: (paths) => {
|
|
6004
|
-
if (!
|
|
7106
|
+
if (!fs35.existsSync(paths.configFile)) return;
|
|
6005
7107
|
const cfg = loadConfig(paths.configFile);
|
|
6006
7108
|
saveConfig(paths.configFile, cfg);
|
|
6007
7109
|
}
|
|
@@ -6062,8 +7164,8 @@ async function runMigrate(opts) {
|
|
|
6062
7164
|
await Promise.resolve(m.apply(paths));
|
|
6063
7165
|
spin.succeed(`${m.name} \u2014 applied`);
|
|
6064
7166
|
applied++;
|
|
6065
|
-
} catch (
|
|
6066
|
-
spin.fail(`${m.name} \u2014 failed: ${
|
|
7167
|
+
} catch (err2) {
|
|
7168
|
+
spin.fail(`${m.name} \u2014 failed: ${err2.message}`);
|
|
6067
7169
|
process.exit(1);
|
|
6068
7170
|
}
|
|
6069
7171
|
}
|
|
@@ -6082,8 +7184,8 @@ import process3 from "process";
|
|
|
6082
7184
|
import { confirm as confirm8 } from "@inquirer/prompts";
|
|
6083
7185
|
|
|
6084
7186
|
// src/core/package-info.ts
|
|
6085
|
-
import
|
|
6086
|
-
import
|
|
7187
|
+
import fs36 from "fs";
|
|
7188
|
+
import path28 from "path";
|
|
6087
7189
|
function changelogBlobUrl(repositoryUrl, version) {
|
|
6088
7190
|
let base = repositoryUrl;
|
|
6089
7191
|
if (!base) base = "https://github.com/AzmSquad/squad-kit";
|
|
@@ -6094,9 +7196,9 @@ function changelogBlobUrl(repositoryUrl, version) {
|
|
|
6094
7196
|
}
|
|
6095
7197
|
function readInstalledPackage() {
|
|
6096
7198
|
const root = packageRoot();
|
|
6097
|
-
const pkgFile =
|
|
6098
|
-
const pkg = JSON.parse(
|
|
6099
|
-
const isDevInstall =
|
|
7199
|
+
const pkgFile = path28.join(root, "package.json");
|
|
7200
|
+
const pkg = JSON.parse(fs36.readFileSync(pkgFile, "utf8"));
|
|
7201
|
+
const isDevInstall = fs36.existsSync(path28.join(root, "src"));
|
|
6100
7202
|
return {
|
|
6101
7203
|
name: pkg.name,
|
|
6102
7204
|
version: pkg.version,
|
|
@@ -6136,7 +7238,7 @@ function isNewer(a, b) {
|
|
|
6136
7238
|
}
|
|
6137
7239
|
|
|
6138
7240
|
// src/core/package-manager.ts
|
|
6139
|
-
import
|
|
7241
|
+
import path29 from "path";
|
|
6140
7242
|
import process2 from "process";
|
|
6141
7243
|
function detectPackageManager(installRoot) {
|
|
6142
7244
|
const ua = process2.env.npm_config_user_agent;
|
|
@@ -6147,11 +7249,11 @@ function detectPackageManager(installRoot) {
|
|
|
6147
7249
|
if (ua.startsWith("npm/")) return { pm: "npm", reason: "npm_config_user_agent" };
|
|
6148
7250
|
}
|
|
6149
7251
|
const root = installRoot.toLowerCase();
|
|
6150
|
-
if (root.includes(`${
|
|
7252
|
+
if (root.includes(`${path29.sep}pnpm${path29.sep}`) || root.includes(`${path29.sep}.pnpm${path29.sep}`))
|
|
6151
7253
|
return { pm: "pnpm", reason: `install path contains pnpm` };
|
|
6152
|
-
if (root.includes(`${
|
|
7254
|
+
if (root.includes(`${path29.sep}.yarn${path29.sep}`) || root.includes(`${path29.sep}yarn${path29.sep}`))
|
|
6153
7255
|
return { pm: "yarn", reason: `install path contains yarn` };
|
|
6154
|
-
if (root.includes(`${
|
|
7256
|
+
if (root.includes(`${path29.sep}.bun${path29.sep}`) || root.includes(`${path29.sep}bun${path29.sep}`))
|
|
6155
7257
|
return { pm: "bun", reason: `install path contains bun` };
|
|
6156
7258
|
return { pm: "npm", reason: "default fallback" };
|
|
6157
7259
|
}
|
|
@@ -6185,9 +7287,9 @@ async function runUpgrade(opts) {
|
|
|
6185
7287
|
try {
|
|
6186
7288
|
const info2 = await fetchLatest(installed.name);
|
|
6187
7289
|
latest = info2.latest;
|
|
6188
|
-
} catch (
|
|
7290
|
+
} catch (err2) {
|
|
6189
7291
|
failure(
|
|
6190
|
-
`Could not reach the npm registry: ${
|
|
7292
|
+
`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
7293
|
);
|
|
6192
7294
|
process3.exit(1);
|
|
6193
7295
|
}
|
|
@@ -6247,7 +7349,7 @@ function spawnInherit(cmd, args) {
|
|
|
6247
7349
|
return new Promise((resolve2, reject) => {
|
|
6248
7350
|
const child = spawn2(cmd, args, { stdio: "inherit", shell: process3.platform === "win32" });
|
|
6249
7351
|
child.on("exit", (code) => resolve2(code ?? 0));
|
|
6250
|
-
child.on("error", (
|
|
7352
|
+
child.on("error", (err2) => reject(err2));
|
|
6251
7353
|
});
|
|
6252
7354
|
}
|
|
6253
7355
|
|
|
@@ -6264,25 +7366,25 @@ import { cors } from "hono/cors";
|
|
|
6264
7366
|
import { secureHeaders } from "hono/secure-headers";
|
|
6265
7367
|
|
|
6266
7368
|
// src/console/api/stories.ts
|
|
6267
|
-
import
|
|
6268
|
-
import { z } from "zod";
|
|
7369
|
+
import fs37 from "fs";
|
|
7370
|
+
import { z as z5 } from "zod";
|
|
6269
7371
|
|
|
6270
7372
|
// src/utils/path-safety.ts
|
|
6271
|
-
import
|
|
7373
|
+
import path30 from "path";
|
|
6272
7374
|
function isInside(abs, parent) {
|
|
6273
|
-
const a =
|
|
6274
|
-
const p =
|
|
7375
|
+
const a = path30.resolve(abs) + path30.sep;
|
|
7376
|
+
const p = path30.resolve(parent) + path30.sep;
|
|
6275
7377
|
return a.startsWith(p);
|
|
6276
7378
|
}
|
|
6277
7379
|
|
|
6278
7380
|
// src/console/api/stories.ts
|
|
6279
|
-
var CreateBody =
|
|
6280
|
-
feature:
|
|
6281
|
-
title:
|
|
6282
|
-
trackerId:
|
|
7381
|
+
var CreateBody = z5.object({
|
|
7382
|
+
feature: z5.string().min(1).max(80),
|
|
7383
|
+
title: z5.string().max(200).optional(),
|
|
7384
|
+
trackerId: z5.string().max(80).optional()
|
|
6283
7385
|
});
|
|
6284
|
-
var PatchBody =
|
|
6285
|
-
intakeContent:
|
|
7386
|
+
var PatchBody = z5.object({
|
|
7387
|
+
intakeContent: z5.string().min(1).max(1e6)
|
|
6286
7388
|
});
|
|
6287
7389
|
function safeFeatureParam(feature) {
|
|
6288
7390
|
return feature.length > 0 && !feature.includes("/") && !feature.includes("..") && slugify(feature) === feature;
|
|
@@ -6311,7 +7413,7 @@ function mountStoriesApi(app, opts) {
|
|
|
6311
7413
|
if (!isInside(hit.intakePath, opts.paths.storiesDir)) return c.json({ error: "forbidden" }, 403);
|
|
6312
7414
|
let intakeContent = "";
|
|
6313
7415
|
try {
|
|
6314
|
-
intakeContent =
|
|
7416
|
+
intakeContent = fs37.readFileSync(hit.intakePath, "utf8");
|
|
6315
7417
|
} catch {
|
|
6316
7418
|
}
|
|
6317
7419
|
return c.json({
|
|
@@ -6355,7 +7457,7 @@ function mountStoriesApi(app, opts) {
|
|
|
6355
7457
|
const hit = stories.find((s) => s.id === id);
|
|
6356
7458
|
if (!hit) return c.json({ error: "not_found" }, 404);
|
|
6357
7459
|
if (!isInside(hit.intakePath, opts.paths.storiesDir)) return c.json({ error: "forbidden" }, 403);
|
|
6358
|
-
|
|
7460
|
+
fs37.writeFileSync(hit.intakePath, body.data.intakeContent, "utf8");
|
|
6359
7461
|
return c.json({ ok: true });
|
|
6360
7462
|
});
|
|
6361
7463
|
app.delete("/api/stories/:feature/:id", (c) => {
|
|
@@ -6372,14 +7474,14 @@ function mountStoriesApi(app, opts) {
|
|
|
6372
7474
|
}
|
|
6373
7475
|
|
|
6374
7476
|
// src/console/api/plans.ts
|
|
6375
|
-
import
|
|
6376
|
-
import
|
|
6377
|
-
import { z as
|
|
7477
|
+
import fs38 from "fs";
|
|
7478
|
+
import path31 from "path";
|
|
7479
|
+
import { z as z6 } from "zod";
|
|
6378
7480
|
import { diffLines } from "diff";
|
|
6379
|
-
var DiffQuery =
|
|
6380
|
-
feature:
|
|
6381
|
-
a:
|
|
6382
|
-
b:
|
|
7481
|
+
var DiffQuery = z6.object({
|
|
7482
|
+
feature: z6.string().min(1).max(80),
|
|
7483
|
+
a: z6.string().min(1).max(200),
|
|
7484
|
+
b: z6.string().min(1).max(200)
|
|
6383
7485
|
});
|
|
6384
7486
|
function safeFeatureParam2(feature) {
|
|
6385
7487
|
return feature.length > 0 && !feature.includes("/") && !feature.includes("..") && slugify(feature) === feature;
|
|
@@ -6388,11 +7490,11 @@ function mountPlansApi(app, opts) {
|
|
|
6388
7490
|
app.get("/api/plans", (c) => {
|
|
6389
7491
|
const out = [];
|
|
6390
7492
|
for (const feature of listPlanFeatureNames(opts.paths)) {
|
|
6391
|
-
const featureDir =
|
|
6392
|
-
if (!
|
|
6393
|
-
for (const entry of
|
|
7493
|
+
const featureDir = path31.join(opts.paths.plansDir, feature);
|
|
7494
|
+
if (!fs38.existsSync(featureDir)) continue;
|
|
7495
|
+
for (const entry of fs38.readdirSync(featureDir)) {
|
|
6394
7496
|
if (!entry.endsWith(".md") || entry === "00-index.md") continue;
|
|
6395
|
-
const abs =
|
|
7497
|
+
const abs = path31.join(featureDir, entry);
|
|
6396
7498
|
out.push({ feature, planFile: entry, metadata: readPlanMetadata(abs) });
|
|
6397
7499
|
}
|
|
6398
7500
|
}
|
|
@@ -6401,10 +7503,10 @@ function mountPlansApi(app, opts) {
|
|
|
6401
7503
|
app.get("/api/plans/:feature/:planFile", (c) => {
|
|
6402
7504
|
const { feature, planFile } = c.req.param();
|
|
6403
7505
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
6404
|
-
const abs =
|
|
7506
|
+
const abs = path31.resolve(path31.join(opts.paths.plansDir, feature, planFile));
|
|
6405
7507
|
if (!isInside(abs, opts.paths.plansDir)) return c.json({ error: "forbidden" }, 403);
|
|
6406
|
-
if (!
|
|
6407
|
-
const content =
|
|
7508
|
+
if (!fs38.existsSync(abs)) return c.json({ error: "not_found" }, 404);
|
|
7509
|
+
const content = fs38.readFileSync(abs, "utf8");
|
|
6408
7510
|
return c.json({
|
|
6409
7511
|
feature,
|
|
6410
7512
|
planFile,
|
|
@@ -6422,15 +7524,15 @@ function mountPlansApi(app, opts) {
|
|
|
6422
7524
|
if (!parse.success) return c.json({ error: "invalid_query", issues: parse.error.issues }, 400);
|
|
6423
7525
|
const { feature, a, b } = parse.data;
|
|
6424
7526
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
6425
|
-
const featureDir =
|
|
6426
|
-
const aAbs =
|
|
6427
|
-
const bAbs =
|
|
7527
|
+
const featureDir = path31.join(opts.paths.plansDir, feature);
|
|
7528
|
+
const aAbs = path31.join(featureDir, a);
|
|
7529
|
+
const bAbs = path31.join(featureDir, b);
|
|
6428
7530
|
if (!isInside(aAbs, opts.paths.plansDir) || !isInside(bAbs, opts.paths.plansDir)) {
|
|
6429
7531
|
return c.json({ error: "forbidden" }, 403);
|
|
6430
7532
|
}
|
|
6431
|
-
if (!
|
|
6432
|
-
const aText =
|
|
6433
|
-
const bText =
|
|
7533
|
+
if (!fs38.existsSync(aAbs) || !fs38.existsSync(bAbs)) return c.json({ error: "not_found" }, 404);
|
|
7534
|
+
const aText = fs38.readFileSync(aAbs, "utf8");
|
|
7535
|
+
const bText = fs38.readFileSync(bAbs, "utf8");
|
|
6434
7536
|
const changes = diffLines(aText, bText);
|
|
6435
7537
|
return c.json({ feature, a, b, changes });
|
|
6436
7538
|
});
|
|
@@ -6438,23 +7540,23 @@ function mountPlansApi(app, opts) {
|
|
|
6438
7540
|
const trash = c.req.query("trash") === "1";
|
|
6439
7541
|
const { feature, planFile } = c.req.param();
|
|
6440
7542
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
6441
|
-
const abs =
|
|
7543
|
+
const abs = path31.join(opts.paths.plansDir, feature, planFile);
|
|
6442
7544
|
if (!isInside(abs, opts.paths.plansDir)) return c.json({ error: "forbidden" }, 403);
|
|
6443
|
-
if (!
|
|
7545
|
+
if (!fs38.existsSync(abs)) return c.json({ error: "not_found" }, 404);
|
|
6444
7546
|
if (trash) {
|
|
6445
7547
|
const result = trashOrDelete([abs], opts.paths.trashDir, true);
|
|
6446
|
-
removeOverviewRowForPlanFile(
|
|
7548
|
+
removeOverviewRowForPlanFile(path31.join(opts.paths.plansDir, feature), planFile);
|
|
6447
7549
|
return c.json({ ok: true, trashed: result.trashed });
|
|
6448
7550
|
}
|
|
6449
|
-
|
|
6450
|
-
removeOverviewRowForPlanFile(
|
|
7551
|
+
fs38.rmSync(abs);
|
|
7552
|
+
removeOverviewRowForPlanFile(path31.join(opts.paths.plansDir, feature), planFile);
|
|
6451
7553
|
return c.json({ ok: true });
|
|
6452
7554
|
});
|
|
6453
7555
|
}
|
|
6454
7556
|
|
|
6455
7557
|
// src/console/api/config.ts
|
|
6456
|
-
import { z as
|
|
6457
|
-
var ConfigBody =
|
|
7558
|
+
import { z as z7 } from "zod";
|
|
7559
|
+
var ConfigBody = z7.object({}).passthrough();
|
|
6458
7560
|
function mountConfigApi(app, opts) {
|
|
6459
7561
|
app.get("/api/config", (c) => {
|
|
6460
7562
|
const config2 = loadConfig(opts.paths.configFile);
|
|
@@ -6466,8 +7568,8 @@ function mountConfigApi(app, opts) {
|
|
|
6466
7568
|
try {
|
|
6467
7569
|
saveConfig(opts.paths.configFile, body.data);
|
|
6468
7570
|
return c.json({ ok: true });
|
|
6469
|
-
} catch (
|
|
6470
|
-
return c.json({ error: "invalid_config", detail:
|
|
7571
|
+
} catch (err2) {
|
|
7572
|
+
return c.json({ error: "invalid_config", detail: err2.message }, 400);
|
|
6471
7573
|
}
|
|
6472
7574
|
});
|
|
6473
7575
|
}
|
|
@@ -6494,14 +7596,14 @@ function mountMetaApi(app, opts) {
|
|
|
6494
7596
|
|
|
6495
7597
|
// src/console/api/runs.ts
|
|
6496
7598
|
import { streamSSE } from "hono/streaming";
|
|
6497
|
-
import { z as
|
|
6498
|
-
import
|
|
6499
|
-
import
|
|
7599
|
+
import { z as z8 } from "zod";
|
|
7600
|
+
import path32 from "path";
|
|
7601
|
+
import fs39 from "fs";
|
|
6500
7602
|
var active = /* @__PURE__ */ new Map();
|
|
6501
7603
|
var EVENT_BUF_CAP = 512;
|
|
6502
|
-
var StartBody =
|
|
6503
|
-
feature:
|
|
6504
|
-
storyId:
|
|
7604
|
+
var StartBody = z8.object({
|
|
7605
|
+
feature: z8.string().min(1),
|
|
7606
|
+
storyId: z8.string().min(1)
|
|
6505
7607
|
});
|
|
6506
7608
|
function emptyStats(durationMs) {
|
|
6507
7609
|
return {
|
|
@@ -6557,11 +7659,36 @@ function mountRunsApi(app, opts) {
|
|
|
6557
7659
|
const done = (async () => {
|
|
6558
7660
|
const cfgFresh = loadConfig(opts.paths.configFile);
|
|
6559
7661
|
const planner = cfgFresh.planner;
|
|
6560
|
-
const
|
|
6561
|
-
const model =
|
|
7662
|
+
const modelId = modelFor(planner.provider, "plan", planner.modelOverride);
|
|
7663
|
+
const { model } = resolveModel(planner.provider, modelId, apiKey);
|
|
6562
7664
|
const budget = new Budget(planner.budget);
|
|
6563
7665
|
const cacheEnabled = planner.cache?.enabled ?? true;
|
|
6564
|
-
const
|
|
7666
|
+
const scoutEnabled = planner.stages?.scout?.enabled !== false;
|
|
7667
|
+
const validationEnabled = planner.validation?.enabled !== false;
|
|
7668
|
+
const strictValidation = planner.validation?.strict === true;
|
|
7669
|
+
const maxScoutFiles = planner.stages?.scout?.maxFiles ?? 12;
|
|
7670
|
+
let scoutModelId = "";
|
|
7671
|
+
let scoutModelHandle;
|
|
7672
|
+
let scoutSystemPrompt;
|
|
7673
|
+
if (scoutEnabled) {
|
|
7674
|
+
scoutModelId = modelFor(
|
|
7675
|
+
planner.provider,
|
|
7676
|
+
"scout",
|
|
7677
|
+
planner.modelOverride,
|
|
7678
|
+
planner.stages?.scout?.modelOverride
|
|
7679
|
+
);
|
|
7680
|
+
scoutModelHandle = resolveModel(planner.provider, scoutModelId, apiKey).model;
|
|
7681
|
+
}
|
|
7682
|
+
const repoMap = buildRepoMap(opts.paths.root, { format: "tree" });
|
|
7683
|
+
if (scoutEnabled) {
|
|
7684
|
+
scoutSystemPrompt = composeScoutSystemPrompt({
|
|
7685
|
+
projectRoots: cfgFresh.project.projectRoots ?? ["."],
|
|
7686
|
+
primaryLanguage: cfgFresh.project.primaryLanguage ?? "",
|
|
7687
|
+
trackerType: cfgFresh.tracker.type,
|
|
7688
|
+
repoMap,
|
|
7689
|
+
plansIndex: buildPlansIndex(opts.paths)
|
|
7690
|
+
});
|
|
7691
|
+
}
|
|
6565
7692
|
const systemPrompt = composeSystemPrompt({
|
|
6566
7693
|
projectRoots: cfgFresh.project.projectRoots ?? ["."],
|
|
6567
7694
|
primaryLanguage: cfgFresh.project.primaryLanguage ?? "",
|
|
@@ -6569,15 +7696,15 @@ function mountRunsApi(app, opts) {
|
|
|
6569
7696
|
repoMap
|
|
6570
7697
|
});
|
|
6571
7698
|
const userPrompt = composeUserPrompt({
|
|
6572
|
-
intakeContent:
|
|
7699
|
+
intakeContent: fs39.readFileSync(story.intakePath, "utf8")
|
|
6573
7700
|
});
|
|
6574
7701
|
let result;
|
|
6575
7702
|
try {
|
|
6576
7703
|
result = await runPlanner({
|
|
6577
7704
|
root: opts.paths.root,
|
|
6578
|
-
provider,
|
|
6579
7705
|
model,
|
|
6580
|
-
|
|
7706
|
+
provider: planner.provider,
|
|
7707
|
+
modelId,
|
|
6581
7708
|
systemPrompt,
|
|
6582
7709
|
userPrompt,
|
|
6583
7710
|
budget,
|
|
@@ -6585,14 +7712,26 @@ function mountRunsApi(app, opts) {
|
|
|
6585
7712
|
cacheEnabled,
|
|
6586
7713
|
events: bus,
|
|
6587
7714
|
runId,
|
|
6588
|
-
abort: controller.signal
|
|
7715
|
+
abort: controller.signal,
|
|
7716
|
+
stages: {
|
|
7717
|
+
scout: {
|
|
7718
|
+
enabled: scoutEnabled,
|
|
7719
|
+
model: scoutModelHandle,
|
|
7720
|
+
modelId: scoutModelId,
|
|
7721
|
+
maxFiles: maxScoutFiles,
|
|
7722
|
+
maxOutputTokens: planner.stages?.scout?.maxOutputTokens ?? 2048
|
|
7723
|
+
}
|
|
7724
|
+
},
|
|
7725
|
+
validation: { enabled: validationEnabled, strict: strictValidation },
|
|
7726
|
+
toolsEnabled: planner.tools,
|
|
7727
|
+
scoutSystemPrompt
|
|
6589
7728
|
});
|
|
6590
|
-
} catch (
|
|
7729
|
+
} catch (err2) {
|
|
6591
7730
|
const durationMs = Date.now() - startedAt;
|
|
6592
7731
|
const last = eventBuffer[eventBuffer.length - 1];
|
|
6593
7732
|
const isRateLimitAbort = last?.kind === "rate_limit" && last.phase === "aborted";
|
|
6594
7733
|
if (!isRateLimitAbort) {
|
|
6595
|
-
bus.emit({ kind: "error", runId, message:
|
|
7734
|
+
bus.emit({ kind: "error", runId, message: err2.message });
|
|
6596
7735
|
}
|
|
6597
7736
|
bus.emit({
|
|
6598
7737
|
kind: "done",
|
|
@@ -6605,20 +7744,25 @@ function mountRunsApi(app, opts) {
|
|
|
6605
7744
|
});
|
|
6606
7745
|
return;
|
|
6607
7746
|
}
|
|
6608
|
-
const
|
|
7747
|
+
const issueCount = result.validation?.issues.length ?? 0;
|
|
7748
|
+
const issuesByKind = summariseIssuesByKind(result.validation?.issues ?? []);
|
|
7749
|
+
const validationBlocks = strictValidation && validationEnabled && issueCount > 0;
|
|
7750
|
+
const runSuccess = result.finishedNormally && !result.timedOut && !result.userCancelled && !validationBlocks;
|
|
6609
7751
|
let planFile = null;
|
|
6610
7752
|
if (result.planText.trim()) {
|
|
6611
7753
|
const snap = budget.snapshot();
|
|
6612
7754
|
const header = buildMetadataHeader({
|
|
6613
7755
|
provider: planner.provider,
|
|
6614
|
-
model,
|
|
7756
|
+
model: modelId,
|
|
6615
7757
|
reads: snap.reads,
|
|
6616
7758
|
bytes: snap.bytes,
|
|
6617
7759
|
inputTokens: snap.usage.inputTokens,
|
|
6618
7760
|
outputTokens: snap.usage.outputTokens,
|
|
6619
7761
|
durationMs: Date.now() - startedAt,
|
|
6620
|
-
|
|
6621
|
-
|
|
7762
|
+
planStatus: runSuccess ? void 0 : "partial",
|
|
7763
|
+
scoutEnabled,
|
|
7764
|
+
validationEnabled,
|
|
7765
|
+
validationIssueCount: issueCount
|
|
6622
7766
|
});
|
|
6623
7767
|
const out = writePlanFile({
|
|
6624
7768
|
paths: opts.paths,
|
|
@@ -6628,19 +7772,19 @@ function mountRunsApi(app, opts) {
|
|
|
6628
7772
|
metadataHeader: header,
|
|
6629
7773
|
partial: !runSuccess
|
|
6630
7774
|
});
|
|
6631
|
-
planFile =
|
|
7775
|
+
planFile = path32.relative(opts.paths.root, out.planFile);
|
|
6632
7776
|
}
|
|
6633
7777
|
try {
|
|
6634
7778
|
await writeLastRun(opts.paths, {
|
|
6635
7779
|
stats: result.stats,
|
|
6636
7780
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6637
7781
|
provider: planner.provider,
|
|
6638
|
-
model
|
|
7782
|
+
model: modelId
|
|
6639
7783
|
});
|
|
6640
7784
|
await appendRun(opts.paths, {
|
|
6641
7785
|
runId,
|
|
6642
7786
|
provider: planner.provider,
|
|
6643
|
-
model,
|
|
7787
|
+
model: modelId,
|
|
6644
7788
|
feature: story.feature,
|
|
6645
7789
|
storyId: story.id,
|
|
6646
7790
|
startedAt: new Date(startedAt).toISOString(),
|
|
@@ -6650,7 +7794,19 @@ function mountRunsApi(app, opts) {
|
|
|
6650
7794
|
planFile,
|
|
6651
7795
|
stats: result.stats,
|
|
6652
7796
|
cacheEnabled,
|
|
6653
|
-
durationMs: Date.now() - startedAt
|
|
7797
|
+
durationMs: Date.now() - startedAt,
|
|
7798
|
+
scout: {
|
|
7799
|
+
enabled: scoutEnabled,
|
|
7800
|
+
selectedCount: result.scout?.selected.length,
|
|
7801
|
+
tokensUsed: result.scout?.tokensUsed,
|
|
7802
|
+
durationMs: result.scout?.durationMs
|
|
7803
|
+
},
|
|
7804
|
+
validation: {
|
|
7805
|
+
enabled: validationEnabled,
|
|
7806
|
+
issuesCount: issueCount,
|
|
7807
|
+
issuesByKind,
|
|
7808
|
+
durationMs: result.validation?.durationMs
|
|
7809
|
+
}
|
|
6654
7810
|
});
|
|
6655
7811
|
} catch {
|
|
6656
7812
|
}
|
|
@@ -6723,7 +7879,7 @@ function mountRunsApi(app, opts) {
|
|
|
6723
7879
|
}
|
|
6724
7880
|
|
|
6725
7881
|
// src/console/api/secrets.ts
|
|
6726
|
-
import { z as
|
|
7882
|
+
import { z as z9 } from "zod";
|
|
6727
7883
|
|
|
6728
7884
|
// src/core/mask-token.ts
|
|
6729
7885
|
function maskToken(t) {
|
|
@@ -6761,26 +7917,26 @@ function mountSecretsApi(app, opts) {
|
|
|
6761
7917
|
}
|
|
6762
7918
|
});
|
|
6763
7919
|
});
|
|
6764
|
-
const PutBody =
|
|
6765
|
-
planner:
|
|
6766
|
-
anthropic:
|
|
6767
|
-
openai:
|
|
6768
|
-
google:
|
|
7920
|
+
const PutBody = z9.object({
|
|
7921
|
+
planner: z9.object({
|
|
7922
|
+
anthropic: z9.string().optional(),
|
|
7923
|
+
openai: z9.string().optional(),
|
|
7924
|
+
google: z9.string().optional()
|
|
6769
7925
|
}).optional(),
|
|
6770
|
-
tracker:
|
|
6771
|
-
jira:
|
|
6772
|
-
host:
|
|
6773
|
-
email:
|
|
6774
|
-
token:
|
|
7926
|
+
tracker: z9.object({
|
|
7927
|
+
jira: z9.object({
|
|
7928
|
+
host: z9.string().optional(),
|
|
7929
|
+
email: z9.string().optional(),
|
|
7930
|
+
token: z9.string().optional()
|
|
6775
7931
|
}).optional(),
|
|
6776
|
-
azure:
|
|
6777
|
-
organization:
|
|
6778
|
-
project:
|
|
6779
|
-
pat:
|
|
7932
|
+
azure: z9.object({
|
|
7933
|
+
organization: z9.string().optional(),
|
|
7934
|
+
project: z9.string().optional(),
|
|
7935
|
+
pat: z9.string().optional()
|
|
6780
7936
|
}).optional(),
|
|
6781
|
-
github:
|
|
6782
|
-
host:
|
|
6783
|
-
pat:
|
|
7937
|
+
github: z9.object({
|
|
7938
|
+
host: z9.string().optional(),
|
|
7939
|
+
pat: z9.string().optional()
|
|
6784
7940
|
}).optional()
|
|
6785
7941
|
}).optional()
|
|
6786
7942
|
});
|
|
@@ -6820,12 +7976,12 @@ function mountSecretsApi(app, opts) {
|
|
|
6820
7976
|
}
|
|
6821
7977
|
|
|
6822
7978
|
// src/console/api/tracker.ts
|
|
6823
|
-
import { z as
|
|
6824
|
-
var SearchQuery =
|
|
6825
|
-
var ImportBody =
|
|
6826
|
-
issueId:
|
|
6827
|
-
feature:
|
|
6828
|
-
withAttachments:
|
|
7979
|
+
import { z as z10 } from "zod";
|
|
7980
|
+
var SearchQuery = z10.object({ q: z10.string().max(500) });
|
|
7981
|
+
var ImportBody = z10.object({
|
|
7982
|
+
issueId: z10.string().min(1),
|
|
7983
|
+
feature: z10.string().min(1).max(80),
|
|
7984
|
+
withAttachments: z10.boolean().optional()
|
|
6829
7985
|
});
|
|
6830
7986
|
function mountTrackerApi(app, opts) {
|
|
6831
7987
|
app.get("/api/tracker/search", async (c) => {
|
|
@@ -6833,18 +7989,18 @@ function mountTrackerApi(app, opts) {
|
|
|
6833
7989
|
if (!q.success) return c.json({ error: "invalid_query" }, 400);
|
|
6834
7990
|
const cfg = loadConfig(opts.paths.configFile);
|
|
6835
7991
|
const secrets = overlayTrackerEnv(loadSecrets(opts.paths.secretsFile));
|
|
6836
|
-
const { client, error
|
|
7992
|
+
const { client, error } = clientFor(cfg, secrets);
|
|
6837
7993
|
if (!client) {
|
|
6838
|
-
return c.json({ error:
|
|
7994
|
+
return c.json({ error: error?.kind ?? "unsupported", detail: error?.detail ?? "" }, 400);
|
|
6839
7995
|
}
|
|
6840
7996
|
try {
|
|
6841
7997
|
const results = await client.searchIssues(q.data.q);
|
|
6842
7998
|
return c.json({ ok: true, results });
|
|
6843
|
-
} catch (
|
|
6844
|
-
if (
|
|
6845
|
-
return c.json({ ok: false, kind:
|
|
7999
|
+
} catch (err2) {
|
|
8000
|
+
if (err2 instanceof TrackerError) {
|
|
8001
|
+
return c.json({ ok: false, kind: err2.kind, status: err2.statusCode, detail: err2.message }, 200);
|
|
6846
8002
|
}
|
|
6847
|
-
return c.json({ ok: false, detail:
|
|
8003
|
+
return c.json({ ok: false, detail: err2.message }, 200);
|
|
6848
8004
|
}
|
|
6849
8005
|
});
|
|
6850
8006
|
app.post("/api/tracker/import", async (c) => {
|
|
@@ -6852,9 +8008,9 @@ function mountTrackerApi(app, opts) {
|
|
|
6852
8008
|
if (!body.success) return c.json({ error: "invalid_body", issues: body.error.issues }, 400);
|
|
6853
8009
|
const cfg = loadConfig(opts.paths.configFile);
|
|
6854
8010
|
const secrets = overlayTrackerEnv(loadSecrets(opts.paths.secretsFile));
|
|
6855
|
-
const { client, error
|
|
8011
|
+
const { client, error } = clientFor(cfg, secrets);
|
|
6856
8012
|
if (!client) {
|
|
6857
|
-
return c.json({ error:
|
|
8013
|
+
return c.json({ error: error?.kind ?? "unsupported", detail: error?.detail ?? "" }, 400);
|
|
6858
8014
|
}
|
|
6859
8015
|
try {
|
|
6860
8016
|
const issue = await client.fetchIssue(body.data.issueId);
|
|
@@ -6867,9 +8023,9 @@ function mountTrackerApi(app, opts) {
|
|
|
6867
8023
|
config: cfg
|
|
6868
8024
|
});
|
|
6869
8025
|
return c.json(created, 201);
|
|
6870
|
-
} catch (
|
|
6871
|
-
if (
|
|
6872
|
-
return c.json({ error: "fetch_failed", detail:
|
|
8026
|
+
} catch (err2) {
|
|
8027
|
+
if (err2 instanceof TrackerError) return c.json({ error: err2.kind, detail: err2.message }, 502);
|
|
8028
|
+
return c.json({ error: "fetch_failed", detail: err2.message }, 500);
|
|
6873
8029
|
}
|
|
6874
8030
|
});
|
|
6875
8031
|
}
|
|
@@ -6966,17 +8122,17 @@ function countStories(stories) {
|
|
|
6966
8122
|
}
|
|
6967
8123
|
|
|
6968
8124
|
// src/console/api/projects.ts
|
|
6969
|
-
import
|
|
6970
|
-
import
|
|
8125
|
+
import fs40 from "fs/promises";
|
|
8126
|
+
import path33 from "path";
|
|
6971
8127
|
import os from "os";
|
|
6972
8128
|
var MAX = 10;
|
|
6973
8129
|
function recentFilePath() {
|
|
6974
|
-
return
|
|
8130
|
+
return path33.join(os.homedir(), ".squad", "recent-projects.json");
|
|
6975
8131
|
}
|
|
6976
8132
|
async function loadRecent() {
|
|
6977
8133
|
const RECENT_FILE = recentFilePath();
|
|
6978
8134
|
try {
|
|
6979
|
-
const raw = await
|
|
8135
|
+
const raw = await fs40.readFile(RECENT_FILE, "utf8");
|
|
6980
8136
|
const parsed = JSON.parse(raw);
|
|
6981
8137
|
return Array.isArray(parsed) ? parsed.slice(0, MAX) : [];
|
|
6982
8138
|
} catch {
|
|
@@ -6985,8 +8141,8 @@ async function loadRecent() {
|
|
|
6985
8141
|
}
|
|
6986
8142
|
async function saveRecent(list) {
|
|
6987
8143
|
const RECENT_FILE = recentFilePath();
|
|
6988
|
-
await
|
|
6989
|
-
await
|
|
8144
|
+
await fs40.mkdir(path33.dirname(RECENT_FILE), { recursive: true });
|
|
8145
|
+
await fs40.writeFile(RECENT_FILE, JSON.stringify(list.slice(0, MAX), null, 2) + "\n", "utf8");
|
|
6990
8146
|
}
|
|
6991
8147
|
function mountProjectsApi(app, opts) {
|
|
6992
8148
|
app.get("/api/projects/recent", async (c) => c.json(await loadRecent()));
|
|
@@ -7015,8 +8171,8 @@ function mountApi(app, opts) {
|
|
|
7015
8171
|
}
|
|
7016
8172
|
|
|
7017
8173
|
// src/console/static-assets.ts
|
|
7018
|
-
import
|
|
7019
|
-
import
|
|
8174
|
+
import fs41 from "fs";
|
|
8175
|
+
import path34 from "path";
|
|
7020
8176
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7021
8177
|
import { Readable } from "stream";
|
|
7022
8178
|
function pathnameForStatic(c) {
|
|
@@ -7046,18 +8202,18 @@ function mountStaticAssets(app, _opts) {
|
|
|
7046
8202
|
}
|
|
7047
8203
|
if (c.req.path.startsWith("/api/") || c.req.path.startsWith("/healthz")) return c.notFound();
|
|
7048
8204
|
if (!distDir) return c.text("UI not built. Run `pnpm -C console-ui build`.", 404);
|
|
7049
|
-
const indexHtml =
|
|
7050
|
-
if (!
|
|
7051
|
-
return c.html(
|
|
8205
|
+
const indexHtml = path34.join(distDir, "index.html");
|
|
8206
|
+
if (!fs41.existsSync(indexHtml)) return c.text("UI not built", 404);
|
|
8207
|
+
return c.html(fs41.readFileSync(indexHtml, "utf8"), 200, { "cache-control": "no-store" });
|
|
7052
8208
|
});
|
|
7053
8209
|
}
|
|
7054
8210
|
function assetResponse(c, distDir, pathname) {
|
|
7055
8211
|
if (!distDir) return c.text("UI not built", 404);
|
|
7056
8212
|
const rel = pathname.replace(/^\//, "");
|
|
7057
|
-
const abs =
|
|
8213
|
+
const abs = path34.normalize(path34.join(distDir, rel));
|
|
7058
8214
|
if (!isInside2(abs, distDir)) return c.text("forbidden", 403);
|
|
7059
|
-
if (!
|
|
7060
|
-
const body = Readable.toWeb(
|
|
8215
|
+
if (!fs41.existsSync(abs) || !fs41.statSync(abs).isFile()) return c.text("not found", 404);
|
|
8216
|
+
const body = Readable.toWeb(fs41.createReadStream(abs));
|
|
7061
8217
|
return new Response(body, {
|
|
7062
8218
|
headers: {
|
|
7063
8219
|
"content-type": contentTypeFor(abs),
|
|
@@ -7067,9 +8223,9 @@ function assetResponse(c, distDir, pathname) {
|
|
|
7067
8223
|
}
|
|
7068
8224
|
function serveFile(c, distDir, name, ct) {
|
|
7069
8225
|
if (!distDir) return c.text("UI not built", 404);
|
|
7070
|
-
const abs =
|
|
7071
|
-
if (!
|
|
7072
|
-
return new Response(
|
|
8226
|
+
const abs = path34.join(distDir, name);
|
|
8227
|
+
if (!fs41.existsSync(abs)) return c.text("not found", 404);
|
|
8228
|
+
return new Response(fs41.readFileSync(abs), {
|
|
7073
8229
|
headers: { "content-type": ct, "cache-control": "public, max-age=86400" }
|
|
7074
8230
|
});
|
|
7075
8231
|
}
|
|
@@ -7084,13 +8240,13 @@ function contentTypeFor(p) {
|
|
|
7084
8240
|
return "application/octet-stream";
|
|
7085
8241
|
}
|
|
7086
8242
|
function isInside2(abs, parent) {
|
|
7087
|
-
const a =
|
|
7088
|
-
const p =
|
|
8243
|
+
const a = path34.resolve(abs) + path34.sep;
|
|
8244
|
+
const p = path34.resolve(parent) + path34.sep;
|
|
7089
8245
|
return a.startsWith(p);
|
|
7090
8246
|
}
|
|
7091
8247
|
function resolveConsoleUiDist() {
|
|
7092
8248
|
const fromEnv = process.env.SQUAD_CONSOLE_UI_DIST;
|
|
7093
|
-
if (fromEnv &&
|
|
8249
|
+
if (fromEnv && fs41.existsSync(path34.join(fromEnv, "index.html"))) return fromEnv;
|
|
7094
8250
|
const candidates = [
|
|
7095
8251
|
fileURLToPath2(new URL("./console-ui/", import.meta.url)),
|
|
7096
8252
|
// dist/cli.js → dist/console-ui/
|
|
@@ -7102,7 +8258,7 @@ function resolveConsoleUiDist() {
|
|
|
7102
8258
|
// src/console/*.ts → project/console-ui/dist
|
|
7103
8259
|
];
|
|
7104
8260
|
for (const c of candidates) {
|
|
7105
|
-
if (
|
|
8261
|
+
if (fs41.existsSync(path34.join(c, "index.html"))) return c;
|
|
7106
8262
|
}
|
|
7107
8263
|
return null;
|
|
7108
8264
|
}
|
|
@@ -7178,13 +8334,13 @@ async function startConsoleServer(opts) {
|
|
|
7178
8334
|
return bindPort;
|
|
7179
8335
|
})();
|
|
7180
8336
|
const shutdown = (onClose) => {
|
|
7181
|
-
nodeServer.close((
|
|
7182
|
-
onClose(
|
|
8337
|
+
nodeServer.close((err2) => {
|
|
8338
|
+
onClose(err2);
|
|
7183
8339
|
});
|
|
7184
8340
|
};
|
|
7185
8341
|
const stop = () => {
|
|
7186
|
-
shutdown((
|
|
7187
|
-
if (
|
|
8342
|
+
shutdown((err2) => {
|
|
8343
|
+
if (err2) warning(`server close: ${err2.message}`);
|
|
7188
8344
|
resolveDone();
|
|
7189
8345
|
});
|
|
7190
8346
|
};
|
|
@@ -7194,9 +8350,9 @@ async function startConsoleServer(opts) {
|
|
|
7194
8350
|
port: actualPort,
|
|
7195
8351
|
done,
|
|
7196
8352
|
close: () => new Promise((resolve2, reject) => {
|
|
7197
|
-
shutdown((
|
|
7198
|
-
if (
|
|
7199
|
-
reject(
|
|
8353
|
+
shutdown((err2) => {
|
|
8354
|
+
if (err2) {
|
|
8355
|
+
reject(err2);
|
|
7200
8356
|
return;
|
|
7201
8357
|
}
|
|
7202
8358
|
resolve2();
|
|
@@ -7284,18 +8440,18 @@ async function runConsole(opts) {
|
|
|
7284
8440
|
}
|
|
7285
8441
|
|
|
7286
8442
|
// src/commands/rm/story.ts
|
|
7287
|
-
import
|
|
7288
|
-
import
|
|
8443
|
+
import fs43 from "fs";
|
|
8444
|
+
import path36 from "path";
|
|
7289
8445
|
import { confirm as confirm10 } from "@inquirer/prompts";
|
|
7290
8446
|
|
|
7291
8447
|
// src/commands/rm/feature.ts
|
|
7292
|
-
import
|
|
7293
|
-
import
|
|
8448
|
+
import fs42 from "fs";
|
|
8449
|
+
import path35 from "path";
|
|
7294
8450
|
import { confirm as confirm9 } from "@inquirer/prompts";
|
|
7295
8451
|
function countStoryDirs(storiesDir, feature) {
|
|
7296
|
-
const d =
|
|
7297
|
-
if (!
|
|
7298
|
-
return
|
|
8452
|
+
const d = path35.join(storiesDir, feature);
|
|
8453
|
+
if (!fs42.existsSync(d)) return 0;
|
|
8454
|
+
return fs42.readdirSync(d, { withFileTypes: true }).filter((x) => x.isDirectory()).length;
|
|
7299
8455
|
}
|
|
7300
8456
|
async function runRmFeature(featureSlug, opts = {}) {
|
|
7301
8457
|
const root = requireSquadRoot();
|
|
@@ -7329,17 +8485,17 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
7329
8485
|
feature = await pickFeatureInteractive(paths);
|
|
7330
8486
|
}
|
|
7331
8487
|
}
|
|
7332
|
-
const storyDir =
|
|
7333
|
-
const planDir =
|
|
8488
|
+
const storyDir = path35.join(paths.storiesDir, feature);
|
|
8489
|
+
const planDir = path35.join(paths.plansDir, feature);
|
|
7334
8490
|
const nStories = countStoryDirs(paths.storiesDir, feature);
|
|
7335
8491
|
const nPlans = listAllPlanListEntries(paths, feature).length;
|
|
7336
|
-
const hasOverview =
|
|
8492
|
+
const hasOverview = fs42.existsSync(path35.join(planDir, "00-overview.md")) ? 1 : 0;
|
|
7337
8493
|
const targets = [];
|
|
7338
|
-
if (
|
|
7339
|
-
targets.push({ label:
|
|
8494
|
+
if (fs42.existsSync(storyDir)) {
|
|
8495
|
+
targets.push({ label: path35.relative(root, storyDir), absPath: storyDir, kind: "dir" });
|
|
7340
8496
|
}
|
|
7341
|
-
if (
|
|
7342
|
-
targets.push({ label:
|
|
8497
|
+
if (fs42.existsSync(planDir)) {
|
|
8498
|
+
targets.push({ label: path35.relative(root, planDir), absPath: planDir, kind: "dir" });
|
|
7343
8499
|
}
|
|
7344
8500
|
divider("squad rm feature");
|
|
7345
8501
|
info(`Feature: ${feature}`);
|
|
@@ -7354,7 +8510,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
7354
8510
|
kv(t.kind, t.label, 6);
|
|
7355
8511
|
}
|
|
7356
8512
|
if (useTrash) {
|
|
7357
|
-
info(`Items will move to ${
|
|
8513
|
+
info(`Items will move to ${path35.relative(process.cwd(), paths.trashDir)}/<timestamp>/`);
|
|
7358
8514
|
} else if (!dryRun) {
|
|
7359
8515
|
warning("This is a permanent delete. Pass --trash to move to .squad/.trash/ instead.");
|
|
7360
8516
|
}
|
|
@@ -7387,7 +8543,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
7387
8543
|
useTrash
|
|
7388
8544
|
);
|
|
7389
8545
|
if (result.trashed) {
|
|
7390
|
-
success(`Feature removed. Items moved to ${
|
|
8546
|
+
success(`Feature removed. Items moved to ${path35.relative(root, result.trashed)}.`);
|
|
7391
8547
|
} else {
|
|
7392
8548
|
success("Feature removed (stories and plans directories).");
|
|
7393
8549
|
}
|
|
@@ -7428,11 +8584,11 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
7428
8584
|
}
|
|
7429
8585
|
}
|
|
7430
8586
|
const targets = [
|
|
7431
|
-
{ label:
|
|
8587
|
+
{ label: path36.relative(root, story.storyDir), absPath: story.storyDir, kind: "dir" }
|
|
7432
8588
|
];
|
|
7433
8589
|
if (story.planFile) {
|
|
7434
|
-
const planAbs =
|
|
7435
|
-
targets.push({ label:
|
|
8590
|
+
const planAbs = path36.join(paths.plansDir, story.feature, story.planFile);
|
|
8591
|
+
targets.push({ label: path36.relative(root, planAbs), absPath: planAbs, kind: "file" });
|
|
7436
8592
|
}
|
|
7437
8593
|
renderPreview("story", targets, {
|
|
7438
8594
|
dryRun,
|
|
@@ -7457,14 +8613,14 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
7457
8613
|
}
|
|
7458
8614
|
const result = deleteStoryRecord({ paths, story, trash: useTrash });
|
|
7459
8615
|
if (result.trashed) {
|
|
7460
|
-
success(`Removed (moved to ${
|
|
8616
|
+
success(`Removed (moved to ${path36.relative(root, result.trashed)}).`);
|
|
7461
8617
|
} else {
|
|
7462
8618
|
success("Removed story folder, plan file (if any), and overview row.");
|
|
7463
8619
|
}
|
|
7464
|
-
const featurePlanDir =
|
|
7465
|
-
const overviewPath =
|
|
7466
|
-
if (
|
|
7467
|
-
const body =
|
|
8620
|
+
const featurePlanDir = path36.join(paths.plansDir, story.feature);
|
|
8621
|
+
const overviewPath = path36.join(featurePlanDir, "00-overview.md");
|
|
8622
|
+
if (fs43.existsSync(overviewPath)) {
|
|
8623
|
+
const body = fs43.readFileSync(overviewPath, "utf8");
|
|
7468
8624
|
const onlyShell = countOverviewDataRows(body) === 0;
|
|
7469
8625
|
if (onlyShell && featureStoriesDirEmpty(paths, story.feature)) {
|
|
7470
8626
|
if (canPrompt) {
|
|
@@ -7484,7 +8640,7 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
7484
8640
|
}
|
|
7485
8641
|
|
|
7486
8642
|
// src/commands/rm/plan.ts
|
|
7487
|
-
import
|
|
8643
|
+
import path37 from "path";
|
|
7488
8644
|
import { confirm as confirm11 } from "@inquirer/prompts";
|
|
7489
8645
|
async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
7490
8646
|
const root = requireSquadRoot();
|
|
@@ -7520,7 +8676,7 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
7520
8676
|
}
|
|
7521
8677
|
const storyRows = listStories(paths, { feature: plan.feature });
|
|
7522
8678
|
const linked = storyRows.find((s) => s.planFile === plan.file);
|
|
7523
|
-
const targets = [{ label:
|
|
8679
|
+
const targets = [{ label: path37.relative(root, plan.abs), absPath: plan.abs, kind: "file" }];
|
|
7524
8680
|
renderPreview("plan", targets, {
|
|
7525
8681
|
dryRun,
|
|
7526
8682
|
trash: useTrash,
|
|
@@ -7543,8 +8699,8 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
7543
8699
|
}
|
|
7544
8700
|
}
|
|
7545
8701
|
trashOrDelete([plan.abs], paths.trashDir, useTrash);
|
|
7546
|
-
removeOverviewRowForPlanFile(
|
|
7547
|
-
const intakeHint = linked ?
|
|
8702
|
+
removeOverviewRowForPlanFile(path37.join(paths.plansDir, plan.feature), plan.file);
|
|
8703
|
+
const intakeHint = linked ? path37.relative(root, linked.intakePath) : `under .squad/stories/${plan.feature}/`;
|
|
7548
8704
|
success(`Plan removed. Run \`squad new-plan ${intakeHint}\` to regenerate.`);
|
|
7549
8705
|
}
|
|
7550
8706
|
|
|
@@ -7578,7 +8734,11 @@ program.command("new-story [feature-slug]").description("Scaffold a new story in
|
|
|
7578
8734
|
"--no-tracker",
|
|
7579
8735
|
"Skip tracker fetch AND drop the tracker-id requirement for this story"
|
|
7580
8736
|
).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).
|
|
8737
|
+
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(
|
|
8738
|
+
"--max-scout-files <n>",
|
|
8739
|
+
"Maximum files the scout can pre-select (default from config or 12)",
|
|
8740
|
+
(v) => parseInt(v, 10)
|
|
8741
|
+
).option("--no-validation", "Disable post-write validation", false).option("--strict-validation", "Write partial plan when validation finds issues", false).action(wrapArgs(runNewPlan));
|
|
7582
8742
|
program.command("status").description("Show squad-kit workspace status").action(wrap(runStatus));
|
|
7583
8743
|
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
8744
|
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 +8768,24 @@ config.command("remove-credential <section>").description('Delete credentials fo
|
|
|
7608
8768
|
(section, opts) => runConfigRemoveCredential(section, { yes: opts.yes })
|
|
7609
8769
|
)
|
|
7610
8770
|
);
|
|
7611
|
-
program.parseAsync(process.argv).catch((
|
|
7612
|
-
if (isSquadExit(
|
|
7613
|
-
if (
|
|
7614
|
-
process.exit(
|
|
8771
|
+
program.parseAsync(process.argv).catch((err2) => {
|
|
8772
|
+
if (isSquadExit(err2)) {
|
|
8773
|
+
if (err2.message.trim()) info(err2.message);
|
|
8774
|
+
process.exit(err2.exitCode);
|
|
7615
8775
|
}
|
|
7616
|
-
renderError(
|
|
8776
|
+
renderError(err2);
|
|
7617
8777
|
process.exit(1);
|
|
7618
8778
|
});
|
|
7619
8779
|
function wrap(fn) {
|
|
7620
8780
|
return async (opts) => {
|
|
7621
8781
|
try {
|
|
7622
8782
|
await fn(opts);
|
|
7623
|
-
} catch (
|
|
7624
|
-
if (isSquadExit(
|
|
7625
|
-
if (
|
|
7626
|
-
process.exit(
|
|
8783
|
+
} catch (err2) {
|
|
8784
|
+
if (isSquadExit(err2)) {
|
|
8785
|
+
if (err2.message.trim()) info(err2.message);
|
|
8786
|
+
process.exit(err2.exitCode);
|
|
7627
8787
|
}
|
|
7628
|
-
renderError(
|
|
8788
|
+
renderError(err2);
|
|
7629
8789
|
process.exit(1);
|
|
7630
8790
|
}
|
|
7631
8791
|
};
|
|
@@ -7634,12 +8794,12 @@ function wrapArgs(fn) {
|
|
|
7634
8794
|
return async (...args) => {
|
|
7635
8795
|
try {
|
|
7636
8796
|
await fn(...args);
|
|
7637
|
-
} catch (
|
|
7638
|
-
if (isSquadExit(
|
|
7639
|
-
if (
|
|
7640
|
-
process.exit(
|
|
8797
|
+
} catch (err2) {
|
|
8798
|
+
if (isSquadExit(err2)) {
|
|
8799
|
+
if (err2.message.trim()) info(err2.message);
|
|
8800
|
+
process.exit(err2.exitCode);
|
|
7641
8801
|
}
|
|
7642
|
-
renderError(
|
|
8802
|
+
renderError(err2);
|
|
7643
8803
|
process.exit(1);
|
|
7644
8804
|
}
|
|
7645
8805
|
};
|