squad-kit 0.7.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/dist/cli.js +2305 -1130
- package/dist/cli.js.map +1 -1
- package/dist/console-ui/assets/index-C5I4cnED.css +1 -0
- package/dist/console-ui/assets/index-Dnb4Fisq.js +416 -0
- package/dist/console-ui/index.html +2 -2
- package/package.json +8 -1
- package/templates/prompts/generate-plan.md +78 -50
- package/templates/prompts/scout.md +42 -0
- package/dist/console-ui/assets/index-BDlxrwaG.css +0 -1
- package/dist/console-ui/assets/index-gaFPpGMN.js +0 -416
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,19 +1703,22 @@ 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`;
|
|
1687
1710
|
}
|
|
1688
|
-
const url = `${this.baseUrl}/rest/api/3/search?jql=${encodeURIComponent(jql)}&fields=summary,status,issuetype&maxResults=${limit}`;
|
|
1711
|
+
const url = `${this.baseUrl}/rest/api/3/search/jql?jql=${encodeURIComponent(jql)}&fields=summary,status,issuetype&maxResults=${limit}`;
|
|
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.`,
|
|
@@ -1734,16 +1760,44 @@ var JiraClient = class {
|
|
|
1734
1760
|
if (status === 429) {
|
|
1735
1761
|
return new TrackerError(`Jira rate limit hit (HTTP 429). Wait a minute and retry.`, "rate-limited", status);
|
|
1736
1762
|
}
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1763
|
+
if (status === 410) {
|
|
1764
|
+
return new TrackerError(
|
|
1765
|
+
id === "search" ? `Jira search endpoint has been removed by Atlassian (HTTP 410). squad-kit must call /rest/api/3/search/jql instead \u2014 upgrade squad-kit to a version that includes this fix.` : `Jira endpoint has been removed by Atlassian (HTTP 410). Upgrade squad-kit to a version that calls the current Jira Cloud REST API.`,
|
|
1766
|
+
"other",
|
|
1767
|
+
status
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
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);
|
|
1742
1773
|
}
|
|
1743
1774
|
};
|
|
1744
1775
|
function stripHtml2(html) {
|
|
1745
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();
|
|
1746
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
|
+
}
|
|
1747
1801
|
|
|
1748
1802
|
// src/tracker/index.ts
|
|
1749
1803
|
function clientFor(config2, secrets) {
|
|
@@ -3101,12 +3155,12 @@ async function tryFetchIssue(config2, id, opts, paths, attachmentsDir) {
|
|
|
3101
3155
|
let issue;
|
|
3102
3156
|
try {
|
|
3103
3157
|
issue = await client.fetchIssue(id);
|
|
3104
|
-
} catch (
|
|
3158
|
+
} catch (err2) {
|
|
3105
3159
|
spin.fail("Tracker fetch failed");
|
|
3106
|
-
if (
|
|
3107
|
-
return { outcome: "failed", reason:
|
|
3160
|
+
if (err2 instanceof TrackerError) {
|
|
3161
|
+
return { outcome: "failed", reason: err2.message, hint: explainTrackerErrorHint(err2) };
|
|
3108
3162
|
}
|
|
3109
|
-
return { outcome: "failed", reason:
|
|
3163
|
+
return { outcome: "failed", reason: err2.message };
|
|
3110
3164
|
}
|
|
3111
3165
|
spin.succeed(`Fetched ${issue.id}: ${truncate(issue.title, 60)}`);
|
|
3112
3166
|
let downloads = [];
|
|
@@ -3128,8 +3182,8 @@ async function tryFetchIssue(config2, id, opts, paths, attachmentsDir) {
|
|
|
3128
3182
|
}
|
|
3129
3183
|
return { outcome: "ok", issue, downloads };
|
|
3130
3184
|
}
|
|
3131
|
-
function explainTrackerErrorHint(
|
|
3132
|
-
switch (
|
|
3185
|
+
function explainTrackerErrorHint(err2) {
|
|
3186
|
+
switch (err2.kind) {
|
|
3133
3187
|
case "auth":
|
|
3134
3188
|
return "Run `squad config set tracker` to re-enter credentials, or `squad doctor` to verify.";
|
|
3135
3189
|
case "not-found":
|
|
@@ -3147,8 +3201,8 @@ function truncate(s, n) {
|
|
|
3147
3201
|
}
|
|
3148
3202
|
|
|
3149
3203
|
// src/commands/new-plan.ts
|
|
3150
|
-
import
|
|
3151
|
-
import
|
|
3204
|
+
import fs31 from "fs";
|
|
3205
|
+
import path23 from "path";
|
|
3152
3206
|
import { confirm as confirm6, select as select6 } from "@inquirer/prompts";
|
|
3153
3207
|
|
|
3154
3208
|
// src/utils/clipboard.ts
|
|
@@ -3176,16 +3230,16 @@ function tryCopy(cmd, text) {
|
|
|
3176
3230
|
let child;
|
|
3177
3231
|
try {
|
|
3178
3232
|
child = spawn(cmd.command, cmd.args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
3179
|
-
} catch (
|
|
3180
|
-
resolve2({ ok: false, reason: `${cmd.command} failed to spawn: ${
|
|
3233
|
+
} catch (err2) {
|
|
3234
|
+
resolve2({ ok: false, reason: `${cmd.command} failed to spawn: ${err2.message}` });
|
|
3181
3235
|
return;
|
|
3182
3236
|
}
|
|
3183
3237
|
let stderr = "";
|
|
3184
3238
|
child.stderr?.on("data", (chunk) => {
|
|
3185
3239
|
stderr += chunk.toString();
|
|
3186
3240
|
});
|
|
3187
|
-
child.on("error", (
|
|
3188
|
-
resolve2({ ok: false, reason: `${cmd.command} spawn error: ${
|
|
3241
|
+
child.on("error", (err2) => {
|
|
3242
|
+
resolve2({ ok: false, reason: `${cmd.command} spawn error: ${err2.message}` });
|
|
3189
3243
|
});
|
|
3190
3244
|
child.on("close", (code) => {
|
|
3191
3245
|
if (code === 0) {
|
|
@@ -3240,63 +3294,189 @@ function noCandidateReason() {
|
|
|
3240
3294
|
}
|
|
3241
3295
|
|
|
3242
3296
|
// src/planner/loop.ts
|
|
3243
|
-
import
|
|
3297
|
+
import path19 from "path";
|
|
3298
|
+
import fs27 from "fs";
|
|
3299
|
+
import { APICallError as APICallError2 } from "ai";
|
|
3244
3300
|
|
|
3245
|
-
// src/planner/tools.ts
|
|
3301
|
+
// src/planner/tools/read-file.ts
|
|
3246
3302
|
import fs21 from "fs";
|
|
3247
3303
|
import path13 from "path";
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
required: ["path"],
|
|
3255
|
-
properties: {
|
|
3256
|
-
path: { type: "string", description: "Repo-relative POSIX path." }
|
|
3257
|
-
}
|
|
3258
|
-
}
|
|
3259
|
-
};
|
|
3260
|
-
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;
|
|
3261
3310
|
function readFileTool(root, budget, input5) {
|
|
3262
|
-
const raw = input5
|
|
3263
|
-
if (
|
|
3264
|
-
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.");
|
|
3265
3325
|
}
|
|
3266
3326
|
const resolved = path13.resolve(root, raw);
|
|
3267
3327
|
const relCheck = path13.relative(root, resolved);
|
|
3268
3328
|
if (relCheck.startsWith("..") || path13.isAbsolute(relCheck)) {
|
|
3269
|
-
return
|
|
3329
|
+
return err(`read_file: path "${raw}" escapes the project root and was refused.`);
|
|
3270
3330
|
}
|
|
3271
3331
|
if (!fs21.existsSync(resolved)) {
|
|
3272
|
-
return
|
|
3332
|
+
return err(`read_file: not found "${raw}".`);
|
|
3273
3333
|
}
|
|
3274
3334
|
const stat = fs21.statSync(resolved);
|
|
3275
3335
|
if (stat.isDirectory()) {
|
|
3276
|
-
return
|
|
3336
|
+
return err(`read_file: "${raw}" is a directory. Ask for a specific file path.`);
|
|
3277
3337
|
}
|
|
3278
|
-
if (
|
|
3279
|
-
return
|
|
3280
|
-
|
|
3338
|
+
if (offset !== void 0 && limit !== void 0) {
|
|
3339
|
+
return readFileRanged(resolved, raw, budget, stat.size, offset, limit);
|
|
3340
|
+
}
|
|
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.`
|
|
3281
3344
|
);
|
|
3282
3345
|
}
|
|
3283
3346
|
const capacity = budget.canRead(stat.size);
|
|
3284
3347
|
if (!capacity.ok) {
|
|
3285
|
-
return
|
|
3348
|
+
return err(`read_file: ${capacity.reason}. Finalise the plan with what you already have.`);
|
|
3286
3349
|
}
|
|
3287
3350
|
let buf;
|
|
3288
3351
|
try {
|
|
3289
3352
|
buf = fs21.readFileSync(resolved);
|
|
3290
3353
|
} catch (e) {
|
|
3291
|
-
return
|
|
3354
|
+
return err(`read_file: failed to read "${raw}" (${e.message}).`);
|
|
3292
3355
|
}
|
|
3293
3356
|
if (looksBinary(buf)) {
|
|
3294
|
-
return
|
|
3357
|
+
return err(`read_file: "${raw}" looks binary and was refused.`);
|
|
3295
3358
|
}
|
|
3296
3359
|
budget.recordRead(stat.size);
|
|
3297
3360
|
return { content: buf.toString("utf8"), isError: false };
|
|
3298
3361
|
}
|
|
3299
|
-
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) {
|
|
3300
3480
|
return { content: msg, isError: true };
|
|
3301
3481
|
}
|
|
3302
3482
|
function looksBinary(buf) {
|
|
@@ -3307,6 +3487,436 @@ function looksBinary(buf) {
|
|
|
3307
3487
|
return false;
|
|
3308
3488
|
}
|
|
3309
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
|
+
|
|
3310
3920
|
// src/planner/provider-errors.ts
|
|
3311
3921
|
function detectModelNotFound(provider, model, status, rawBody) {
|
|
3312
3922
|
if (status !== 404) return void 0;
|
|
@@ -3315,16 +3925,16 @@ function detectModelNotFound(provider, model, status, rawBody) {
|
|
|
3315
3925
|
if (!hints.some((h) => body.includes(h))) return void 0;
|
|
3316
3926
|
return { provider, model, status, rawBody };
|
|
3317
3927
|
}
|
|
3318
|
-
function modelNotFoundMessage(
|
|
3928
|
+
function modelNotFoundMessage(err2) {
|
|
3319
3929
|
return [
|
|
3320
|
-
`The ${
|
|
3930
|
+
`The ${err2.provider} planner model "${err2.model}" is no longer available.`,
|
|
3321
3931
|
"",
|
|
3322
3932
|
"Recovery options:",
|
|
3323
3933
|
` 1. Run \`squad upgrade\` to install a newer squad-kit (or install manually from npm).`,
|
|
3324
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.`,
|
|
3325
|
-
` 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.`,
|
|
3326
3936
|
"",
|
|
3327
|
-
`Raw provider response: ${
|
|
3937
|
+
`Raw provider response: ${err2.rawBody.slice(0, 200)}`
|
|
3328
3938
|
].join("\n");
|
|
3329
3939
|
}
|
|
3330
3940
|
function detectRateLimit(provider, status, headers, rawBody) {
|
|
@@ -3336,16 +3946,20 @@ function detectRateLimit(provider, status, headers, rawBody) {
|
|
|
3336
3946
|
rawBody
|
|
3337
3947
|
};
|
|
3338
3948
|
}
|
|
3339
|
-
function
|
|
3949
|
+
function parseRetryAfterSec(headers) {
|
|
3340
3950
|
const raw = headerGet(headers, "retry-after");
|
|
3341
|
-
if (raw)
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
}
|
|
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));
|
|
3348
3957
|
}
|
|
3958
|
+
return void 0;
|
|
3959
|
+
}
|
|
3960
|
+
function parseRetryAfter(headers, body) {
|
|
3961
|
+
const fromHeader = parseRetryAfterSec(headers);
|
|
3962
|
+
if (fromHeader !== void 0) return fromHeader;
|
|
3349
3963
|
const bodyMatch = /"retryDelay"\s*:\s*"(\d+)(?:\.\d+)?s"/i.exec(body);
|
|
3350
3964
|
if (bodyMatch) {
|
|
3351
3965
|
const sec = Number(bodyMatch[1]);
|
|
@@ -3364,16 +3978,16 @@ function headerGet(h, name) {
|
|
|
3364
3978
|
}
|
|
3365
3979
|
return null;
|
|
3366
3980
|
}
|
|
3367
|
-
function rateLimitMessage(
|
|
3368
|
-
const waitHint =
|
|
3981
|
+
function rateLimitMessage(err2) {
|
|
3982
|
+
const waitHint = err2.retryAfterSec ?? 60;
|
|
3369
3983
|
const limitsUrl = {
|
|
3370
3984
|
anthropic: "https://console.anthropic.com/settings/limits",
|
|
3371
3985
|
openai: "https://platform.openai.com/settings/organization/limits",
|
|
3372
3986
|
google: "https://aistudio.google.com/app/plan_information"
|
|
3373
3987
|
};
|
|
3374
|
-
const headline =
|
|
3375
|
-
const cap =
|
|
3376
|
-
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.";
|
|
3377
3991
|
return [
|
|
3378
3992
|
headline,
|
|
3379
3993
|
retried,
|
|
@@ -3382,427 +3996,148 @@ function rateLimitMessage(err) {
|
|
|
3382
3996
|
` 1. Wait ${waitHint}s and rerun \`squad new-plan --api\`. Per-minute limits reset quickly.`,
|
|
3383
3997
|
` 2. Switch to a smaller planner model: \`squad config set planner\` (pick a cheaper model id).`,
|
|
3384
3998
|
` 3. Tighten \`planner.budget\` in \`.squad/config.yaml\` (smaller \`maxContextBytes\` / \`maxFileReads\`) so each request carries fewer tokens.`,
|
|
3385
|
-
` 4. Upgrade your ${
|
|
3999
|
+
` 4. Upgrade your ${err2.provider} tier: ${limitsUrl[err2.provider]}.`,
|
|
3386
4000
|
"",
|
|
3387
4001
|
"Full runbook: docs/migrating-from-0.1.md (see \xA78. If something goes wrong).",
|
|
3388
4002
|
"",
|
|
3389
|
-
`Raw provider response: ${
|
|
4003
|
+
`Raw provider response: ${err2.rawBody.slice(0, 200)}`
|
|
3390
4004
|
].join("\n");
|
|
3391
4005
|
}
|
|
3392
4006
|
|
|
3393
|
-
// src/planner/
|
|
3394
|
-
function
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
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;
|
|
3401
4018
|
}
|
|
3402
4019
|
return out;
|
|
3403
4020
|
}
|
|
3404
4021
|
|
|
3405
|
-
// src/planner/
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
system: [
|
|
3431
|
-
{
|
|
3432
|
-
type: "text",
|
|
3433
|
-
text: req.systemPrompt,
|
|
3434
|
-
cache_control: { type: "ephemeral" }
|
|
3435
|
-
}
|
|
3436
|
-
],
|
|
3437
|
-
tools: req.tools.map(toAnthropicTool)
|
|
3438
|
-
};
|
|
3439
|
-
if (req.turns.length === 0) {
|
|
3440
|
-
return base;
|
|
3441
|
-
}
|
|
3442
|
-
return { ...base, messages: toMessagesWithCacheMarkers(req.turns) };
|
|
3443
|
-
}
|
|
3444
|
-
async function callAnthropic(req) {
|
|
3445
|
-
const built = buildAnthropicBody(req);
|
|
3446
|
-
const body = { ...built, messages: built.messages ?? [] };
|
|
3447
|
-
const res = await fetch(API_URL, {
|
|
3448
|
-
method: "POST",
|
|
3449
|
-
headers: {
|
|
3450
|
-
"x-api-key": req.apiKey,
|
|
3451
|
-
"anthropic-version": API_VERSION,
|
|
3452
|
-
"content-type": "application/json"
|
|
3453
|
-
},
|
|
3454
|
-
body: JSON.stringify(body),
|
|
3455
|
-
signal: req.abort
|
|
3456
|
-
});
|
|
3457
|
-
if (!res.ok) {
|
|
3458
|
-
const text = await res.text().catch(() => "");
|
|
3459
|
-
const nf = detectModelNotFound("anthropic", req.model, res.status, text);
|
|
3460
|
-
if (nf) {
|
|
3461
|
-
return {
|
|
3462
|
-
stopReason: "error",
|
|
3463
|
-
rawError: modelNotFoundMessage(nf),
|
|
3464
|
-
errorKind: "model_not_found"
|
|
3465
|
-
};
|
|
3466
|
-
}
|
|
3467
|
-
const rl = detectRateLimit("anthropic", res.status, res.headers, text);
|
|
3468
|
-
if (rl) {
|
|
3469
|
-
return {
|
|
3470
|
-
stopReason: "error",
|
|
3471
|
-
rawError: `anthropic 429: ${text.slice(0, 500)}`,
|
|
3472
|
-
errorKind: "rate_limit",
|
|
3473
|
-
retryAfterSec: rl.retryAfterSec
|
|
3474
|
-
};
|
|
3475
|
-
}
|
|
3476
|
-
return { stopReason: "error", rawError: `anthropic ${res.status}: ${text.slice(0, 500)}` };
|
|
3477
|
-
}
|
|
3478
|
-
const json = await res.json();
|
|
3479
|
-
const textOut = json.content.filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
3480
|
-
const toolCalls = json.content.filter((b) => b.type === "tool_use").map((b) => ({ id: b.id, name: b.name, input: b.input }));
|
|
3481
|
-
return {
|
|
3482
|
-
text: textOut || void 0,
|
|
3483
|
-
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
3484
|
-
stopReason: mapStop(json.stop_reason),
|
|
3485
|
-
usage: {
|
|
3486
|
-
inputTokens: json.usage?.input_tokens ?? 0,
|
|
3487
|
-
outputTokens: json.usage?.output_tokens ?? 0,
|
|
3488
|
-
cacheCreationTokens: json.usage?.cache_creation_input_tokens ?? 0,
|
|
3489
|
-
cacheReadTokens: json.usage?.cache_read_input_tokens ?? 0
|
|
3490
|
-
}
|
|
3491
|
-
};
|
|
3492
|
-
}
|
|
3493
|
-
function toMessagesWithCacheMarkers(turns) {
|
|
3494
|
-
let lastUserWithToolResultIdx = -1;
|
|
3495
|
-
for (let i = turns.length - 1; i >= 0; i -= 1) {
|
|
3496
|
-
const t = turns[i];
|
|
3497
|
-
if (t.role === "user" && hasToolResultBlock(t)) {
|
|
3498
|
-
lastUserWithToolResultIdx = i;
|
|
3499
|
-
break;
|
|
3500
|
-
}
|
|
3501
|
-
}
|
|
3502
|
-
return turns.map((turn, idx) => {
|
|
3503
|
-
const message = toAnthropicMessage(turn);
|
|
3504
|
-
if (idx === lastUserWithToolResultIdx) {
|
|
3505
|
-
return attachCacheControlToLastToolResult(message);
|
|
3506
|
-
}
|
|
3507
|
-
return message;
|
|
3508
|
-
});
|
|
3509
|
-
}
|
|
3510
|
-
function hasToolResultBlock(turn) {
|
|
3511
|
-
return (turn.toolResults?.length ?? 0) > 0;
|
|
3512
|
-
}
|
|
3513
|
-
function attachCacheControlToLastToolResult(msg) {
|
|
3514
|
-
const content = [...msg.content];
|
|
3515
|
-
for (let i = content.length - 1; i >= 0; i -= 1) {
|
|
3516
|
-
const block = content[i];
|
|
3517
|
-
if (block.type === "tool_result") {
|
|
3518
|
-
content[i] = { ...block, cache_control: { type: "ephemeral" } };
|
|
3519
|
-
break;
|
|
3520
|
-
}
|
|
3521
|
-
}
|
|
3522
|
-
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;
|
|
3523
4047
|
}
|
|
3524
|
-
function
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
input_schema: sortRecordKeys(t.inputSchema)
|
|
3529
|
-
};
|
|
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;
|
|
3530
4052
|
}
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
name: tc.name,
|
|
3540
|
-
input: sortRecordKeys(tc.input)
|
|
3541
|
-
});
|
|
3542
|
-
}
|
|
3543
|
-
}
|
|
3544
|
-
if (turn.toolResults) {
|
|
3545
|
-
for (const tr of turn.toolResults) {
|
|
3546
|
-
blocks.push({
|
|
3547
|
-
type: "tool_result",
|
|
3548
|
-
tool_use_id: tr.toolCallId,
|
|
3549
|
-
content: tr.content,
|
|
3550
|
-
is_error: tr.isError ?? false
|
|
3551
|
-
});
|
|
3552
|
-
}
|
|
3553
|
-
}
|
|
3554
|
-
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");
|
|
3555
4061
|
}
|
|
3556
|
-
function
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
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");
|
|
3560
4071
|
}
|
|
3561
4072
|
|
|
3562
|
-
// src/planner/
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
}
|
|
3570
|
-
function buildOpenAIBody(req) {
|
|
3571
|
-
const base = {
|
|
3572
|
-
model: req.model,
|
|
3573
|
-
max_completion_tokens: req.maxOutputTokens ?? 4096,
|
|
3574
|
-
tools: req.tools.map(toOpenAITool)
|
|
3575
|
-
};
|
|
3576
|
-
if (req.turns.length === 0) {
|
|
3577
|
-
return base;
|
|
3578
|
-
}
|
|
3579
|
-
const messages = [{ role: "system", content: req.systemPrompt }];
|
|
3580
|
-
for (const turn of req.turns) messages.push(...toOpenAIMessages(turn));
|
|
3581
|
-
return { ...base, messages };
|
|
3582
|
-
}
|
|
3583
|
-
async function callOpenAI(req) {
|
|
3584
|
-
const built = buildOpenAIBody(req);
|
|
3585
|
-
const messages = built.messages && built.messages.length > 0 ? built.messages : [{ role: "system", content: req.systemPrompt }];
|
|
3586
|
-
const body = { ...built, messages };
|
|
3587
|
-
const res = await fetch(API_URL2, {
|
|
3588
|
-
method: "POST",
|
|
3589
|
-
headers: {
|
|
3590
|
-
Authorization: `Bearer ${req.apiKey}`,
|
|
3591
|
-
"content-type": "application/json"
|
|
3592
|
-
},
|
|
3593
|
-
body: JSON.stringify(body),
|
|
3594
|
-
signal: req.abort
|
|
3595
|
-
});
|
|
3596
|
-
if (!res.ok) {
|
|
3597
|
-
const text = await res.text().catch(() => "");
|
|
3598
|
-
const nf = detectModelNotFound("openai", req.model, res.status, text);
|
|
3599
|
-
if (nf) {
|
|
3600
|
-
return {
|
|
3601
|
-
stopReason: "error",
|
|
3602
|
-
rawError: modelNotFoundMessage(nf),
|
|
3603
|
-
errorKind: "model_not_found"
|
|
3604
|
-
};
|
|
3605
|
-
}
|
|
3606
|
-
const rl = detectRateLimit("openai", res.status, res.headers, text);
|
|
3607
|
-
if (rl) {
|
|
3608
|
-
return {
|
|
3609
|
-
stopReason: "error",
|
|
3610
|
-
rawError: `openai 429: ${text.slice(0, 500)}`,
|
|
3611
|
-
errorKind: "rate_limit",
|
|
3612
|
-
retryAfterSec: rl.retryAfterSec
|
|
3613
|
-
};
|
|
3614
|
-
}
|
|
3615
|
-
return { stopReason: "error", rawError: `openai ${res.status}: ${text.slice(0, 500)}` };
|
|
3616
|
-
}
|
|
3617
|
-
const json = await res.json();
|
|
3618
|
-
const choice = json.choices?.[0];
|
|
3619
|
-
if (!choice) return { stopReason: "error", rawError: "openai: empty choices" };
|
|
3620
|
-
const textOut = choice.message.content ?? "";
|
|
3621
|
-
const toolCalls = (choice.message.tool_calls ?? []).map((tc) => ({
|
|
3622
|
-
id: tc.id,
|
|
3623
|
-
name: tc.function.name,
|
|
3624
|
-
input: safeJson(tc.function.arguments)
|
|
3625
|
-
}));
|
|
3626
|
-
return {
|
|
3627
|
-
text: textOut || void 0,
|
|
3628
|
-
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
3629
|
-
stopReason: mapStop2(choice.finish_reason),
|
|
3630
|
-
usage: {
|
|
3631
|
-
inputTokens: json.usage?.prompt_tokens ?? 0,
|
|
3632
|
-
outputTokens: json.usage?.completion_tokens ?? 0
|
|
3633
|
-
}
|
|
3634
|
-
};
|
|
3635
|
-
}
|
|
3636
|
-
function toOpenAITool(t) {
|
|
3637
|
-
return {
|
|
3638
|
-
type: "function",
|
|
3639
|
-
function: {
|
|
3640
|
-
name: t.name,
|
|
3641
|
-
description: t.description,
|
|
3642
|
-
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" } } } } : {}
|
|
3643
4081
|
}
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
type: "function",
|
|
3655
|
-
function: {
|
|
3656
|
-
name: tc.name,
|
|
3657
|
-
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
|
+
};
|
|
3658
4092
|
}
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
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 });
|
|
3666
4124
|
}
|
|
3667
4125
|
}
|
|
3668
4126
|
}
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
return "end_turn";
|
|
3675
|
-
}
|
|
3676
|
-
function safeJson(raw) {
|
|
3677
|
-
try {
|
|
3678
|
-
return sortRecordKeys(JSON.parse(raw));
|
|
3679
|
-
} catch {
|
|
3680
|
-
return {};
|
|
3681
|
-
}
|
|
3682
|
-
}
|
|
3683
|
-
|
|
3684
|
-
// src/planner/providers/google.ts
|
|
3685
|
-
var API_BASE = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
3686
|
-
var googleProvider = {
|
|
3687
|
-
name: "google",
|
|
3688
|
-
async send(req) {
|
|
3689
|
-
return callGoogle(req);
|
|
3690
|
-
}
|
|
3691
|
-
};
|
|
3692
|
-
function buildGoogleBody(req) {
|
|
3693
|
-
const base = {
|
|
3694
|
-
system_instruction: { parts: [{ text: req.systemPrompt }] },
|
|
3695
|
-
generationConfig: { maxOutputTokens: req.maxOutputTokens ?? 4096 }
|
|
3696
|
-
};
|
|
3697
|
-
if (req.tools.length) {
|
|
3698
|
-
base.tools = [{ functionDeclarations: req.tools.map(toGoogleTool) }];
|
|
3699
|
-
}
|
|
3700
|
-
if (req.turns.length === 0) {
|
|
3701
|
-
return base;
|
|
3702
|
-
}
|
|
3703
|
-
return { ...base, contents: req.turns.map(toGoogleContent) };
|
|
3704
|
-
}
|
|
3705
|
-
async function callGoogle(req) {
|
|
3706
|
-
const url = `${API_BASE}/${encodeURIComponent(req.model)}:generateContent?key=${encodeURIComponent(req.apiKey)}`;
|
|
3707
|
-
const built = buildGoogleBody(req);
|
|
3708
|
-
const body = { ...built, contents: built.contents ?? [] };
|
|
3709
|
-
const res = await fetch(url, {
|
|
3710
|
-
method: "POST",
|
|
3711
|
-
headers: { "content-type": "application/json" },
|
|
3712
|
-
body: JSON.stringify(body),
|
|
3713
|
-
signal: req.abort
|
|
3714
|
-
});
|
|
3715
|
-
if (!res.ok) {
|
|
3716
|
-
const text = await res.text().catch(() => "");
|
|
3717
|
-
const nf = detectModelNotFound("google", req.model, res.status, text);
|
|
3718
|
-
if (nf) {
|
|
3719
|
-
return {
|
|
3720
|
-
stopReason: "error",
|
|
3721
|
-
rawError: modelNotFoundMessage(nf),
|
|
3722
|
-
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" } }
|
|
3723
4132
|
};
|
|
3724
4133
|
}
|
|
3725
|
-
const rl = detectRateLimit("google", res.status, res.headers, text);
|
|
3726
|
-
if (rl) {
|
|
3727
|
-
return {
|
|
3728
|
-
stopReason: "error",
|
|
3729
|
-
rawError: `google 429: ${text.slice(0, 500)}`,
|
|
3730
|
-
errorKind: "rate_limit",
|
|
3731
|
-
retryAfterSec: rl.retryAfterSec
|
|
3732
|
-
};
|
|
3733
|
-
}
|
|
3734
|
-
return { stopReason: "error", rawError: `google ${res.status}: ${text.slice(0, 500)}` };
|
|
3735
|
-
}
|
|
3736
|
-
const json = await res.json();
|
|
3737
|
-
const cand = json.candidates?.[0];
|
|
3738
|
-
if (!cand) return { stopReason: "error", rawError: "google: empty candidates" };
|
|
3739
|
-
const parts = cand.content?.parts ?? [];
|
|
3740
|
-
const textOut = parts.filter((p) => "text" in p && !!p.text).map((p) => p.text).join("");
|
|
3741
|
-
const toolCalls = parts.filter((p) => "functionCall" in p && !!p.functionCall).map((p, i) => ({
|
|
3742
|
-
id: `gcall_${i}_${p.functionCall.name}`,
|
|
3743
|
-
name: p.functionCall.name,
|
|
3744
|
-
input: p.functionCall.args ? sortRecordKeys(p.functionCall.args) : {}
|
|
3745
|
-
}));
|
|
3746
|
-
return {
|
|
3747
|
-
text: textOut || void 0,
|
|
3748
|
-
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
3749
|
-
stopReason: mapStop3(cand.finishReason),
|
|
3750
|
-
usage: {
|
|
3751
|
-
inputTokens: json.usageMetadata?.promptTokenCount ?? 0,
|
|
3752
|
-
outputTokens: json.usageMetadata?.candidatesTokenCount ?? 0
|
|
3753
|
-
}
|
|
3754
|
-
};
|
|
3755
|
-
}
|
|
3756
|
-
function toGoogleTool(t) {
|
|
3757
|
-
return { name: t.name, description: t.description, parameters: sortRecordKeys(t.inputSchema) };
|
|
3758
|
-
}
|
|
3759
|
-
function toGoogleContent(turn) {
|
|
3760
|
-
const role = turn.role === "assistant" ? "model" : "user";
|
|
3761
|
-
const parts = [];
|
|
3762
|
-
if (turn.text) parts.push({ text: turn.text });
|
|
3763
|
-
if (turn.toolCalls) {
|
|
3764
|
-
for (const tc of turn.toolCalls) {
|
|
3765
|
-
parts.push({ functionCall: { name: tc.name, args: sortRecordKeys(tc.input) } });
|
|
3766
|
-
}
|
|
3767
4134
|
}
|
|
3768
|
-
|
|
3769
|
-
for (const tr of turn.toolResults) {
|
|
3770
|
-
parts.push({
|
|
3771
|
-
functionResponse: { name: nameFromToolCallId(tr.toolCallId), response: { content: tr.content } }
|
|
3772
|
-
});
|
|
3773
|
-
}
|
|
3774
|
-
}
|
|
3775
|
-
return { role, parts };
|
|
3776
|
-
}
|
|
3777
|
-
function nameFromToolCallId(id) {
|
|
3778
|
-
const m = id.match(/^gcall_\d+_(.+)$/);
|
|
3779
|
-
return m?.[1] ?? "read_file";
|
|
3780
|
-
}
|
|
3781
|
-
function mapStop3(r) {
|
|
3782
|
-
if (r === "STOP" || r === "END_OF_TURN" || r === "FINISH_REASON_STOP") return "end_turn";
|
|
3783
|
-
if (r === "MAX_TOKENS") return "max_tokens";
|
|
3784
|
-
return "tool_use";
|
|
3785
|
-
}
|
|
3786
|
-
|
|
3787
|
-
// src/planner/providers/prefix.ts
|
|
3788
|
-
function prefixOf(provider, req) {
|
|
3789
|
-
const trimmed = { ...req, turns: req.turns.slice(0, -1) };
|
|
3790
|
-
const s = (() => {
|
|
3791
|
-
switch (provider) {
|
|
3792
|
-
case "anthropic":
|
|
3793
|
-
return JSON.stringify(buildAnthropicBody(trimmed));
|
|
3794
|
-
case "openai":
|
|
3795
|
-
return JSON.stringify(buildOpenAIBody(trimmed));
|
|
3796
|
-
case "google":
|
|
3797
|
-
return JSON.stringify(buildGoogleBody(trimmed));
|
|
3798
|
-
}
|
|
3799
|
-
})();
|
|
3800
|
-
return s.length > 0 ? s.slice(0, -1) : s;
|
|
4135
|
+
return messages;
|
|
3801
4136
|
}
|
|
3802
4137
|
|
|
3803
4138
|
// src/core/runs.ts
|
|
3804
|
-
import
|
|
3805
|
-
import
|
|
4139
|
+
import fs25 from "fs/promises";
|
|
4140
|
+
import path17 from "path";
|
|
3806
4141
|
import { randomBytes } from "crypto";
|
|
3807
4142
|
var RING_SIZE = 20;
|
|
3808
4143
|
function newRunId() {
|
|
@@ -3811,24 +4146,24 @@ function newRunId() {
|
|
|
3811
4146
|
return `${ts}-${rnd}`;
|
|
3812
4147
|
}
|
|
3813
4148
|
async function appendRun(paths, rec) {
|
|
3814
|
-
const dir =
|
|
3815
|
-
await
|
|
3816
|
-
const file =
|
|
3817
|
-
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");
|
|
3818
4153
|
await pruneOldRuns(dir);
|
|
3819
4154
|
}
|
|
3820
4155
|
async function pruneOldRuns(dir) {
|
|
3821
|
-
const entries = (await
|
|
4156
|
+
const entries = (await fs25.readdir(dir)).filter((n) => n.endsWith(".json"));
|
|
3822
4157
|
if (entries.length <= RING_SIZE) return;
|
|
3823
4158
|
entries.sort();
|
|
3824
4159
|
const drop = entries.slice(0, entries.length - RING_SIZE);
|
|
3825
|
-
await Promise.all(drop.map((n) =>
|
|
4160
|
+
await Promise.all(drop.map((n) => fs25.rm(path17.join(dir, n), { force: true })));
|
|
3826
4161
|
}
|
|
3827
4162
|
async function listRuns(paths) {
|
|
3828
|
-
const dir =
|
|
4163
|
+
const dir = path17.join(paths.squadDir, "runs");
|
|
3829
4164
|
let entries;
|
|
3830
4165
|
try {
|
|
3831
|
-
entries = await
|
|
4166
|
+
entries = await fs25.readdir(dir);
|
|
3832
4167
|
} catch {
|
|
3833
4168
|
return [];
|
|
3834
4169
|
}
|
|
@@ -3836,7 +4171,7 @@ async function listRuns(paths) {
|
|
|
3836
4171
|
const out = [];
|
|
3837
4172
|
for (const f of files) {
|
|
3838
4173
|
try {
|
|
3839
|
-
const raw = await
|
|
4174
|
+
const raw = await fs25.readFile(path17.join(dir, f), "utf8");
|
|
3840
4175
|
const parsed = JSON.parse(raw);
|
|
3841
4176
|
if (parsed.version === 1) out.push(parsed);
|
|
3842
4177
|
} catch {
|
|
@@ -3866,6 +4201,355 @@ var PlannerEventBus = class {
|
|
|
3866
4201
|
}
|
|
3867
4202
|
};
|
|
3868
4203
|
|
|
4204
|
+
// src/planner/stages/scout.ts
|
|
4205
|
+
import { APICallError, generateObject, NoObjectGeneratedError } from "ai";
|
|
4206
|
+
import { z as z4 } from "zod";
|
|
4207
|
+
var ScoutOutputSchema = z4.object({
|
|
4208
|
+
selectedFiles: z4.array(z4.string()).min(1).max(25),
|
|
4209
|
+
reasoning: z4.string().min(1),
|
|
4210
|
+
suggestedReadStrategy: z4.enum(["read_full", "read_ranges", "mixed"]),
|
|
4211
|
+
readRanges: z4.array(
|
|
4212
|
+
z4.object({
|
|
4213
|
+
path: z4.string(),
|
|
4214
|
+
offset: z4.number().int().min(1),
|
|
4215
|
+
limit: z4.number().int().min(1).max(400)
|
|
4216
|
+
})
|
|
4217
|
+
).optional()
|
|
4218
|
+
});
|
|
4219
|
+
async function runScout(input5) {
|
|
4220
|
+
const maxTokens = input5.maxTokens ?? 2048;
|
|
4221
|
+
let attempt = 0;
|
|
4222
|
+
while (true) {
|
|
4223
|
+
try {
|
|
4224
|
+
const { object, usage, providerMetadata } = await generateObject({
|
|
4225
|
+
model: input5.model,
|
|
4226
|
+
system: input5.systemPrompt,
|
|
4227
|
+
prompt: input5.userPrompt,
|
|
4228
|
+
schema: ScoutOutputSchema,
|
|
4229
|
+
maxTokens,
|
|
4230
|
+
abortSignal: input5.abort
|
|
4231
|
+
});
|
|
4232
|
+
const u = usageFromLanguageModelStep(usage, providerMetadata);
|
|
4233
|
+
input5.budget.recordUsage(u);
|
|
4234
|
+
return {
|
|
4235
|
+
output: object,
|
|
4236
|
+
usage: { inputTokens: u.inputTokens, outputTokens: u.outputTokens }
|
|
4237
|
+
};
|
|
4238
|
+
} catch (e) {
|
|
4239
|
+
if (NoObjectGeneratedError.isInstance(e)) {
|
|
4240
|
+
return null;
|
|
4241
|
+
}
|
|
4242
|
+
if (APICallError.isInstance(e) && attempt === 0) {
|
|
4243
|
+
const status = e.statusCode ?? 0;
|
|
4244
|
+
if (status === 429) {
|
|
4245
|
+
const ra = parseRetryAfterSec(e.responseHeaders ?? {});
|
|
4246
|
+
if (ra !== void 0 && ra <= 90) {
|
|
4247
|
+
await new Promise((r) => setTimeout(r, ra * 1e3));
|
|
4248
|
+
attempt += 1;
|
|
4249
|
+
continue;
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
throw e;
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
|
|
4258
|
+
// src/planner/validation.ts
|
|
4259
|
+
import fs26 from "fs";
|
|
4260
|
+
import path18 from "path";
|
|
4261
|
+
var CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|md|json|ya?ml|css|html|py|go|rs|cs|java|kt|kts|swift|rb|php|vue|svelte)$/i;
|
|
4262
|
+
function summariseIssuesByKind(issues) {
|
|
4263
|
+
const out = {};
|
|
4264
|
+
for (const i of issues) {
|
|
4265
|
+
out[i.kind] = (out[i.kind] ?? 0) + 1;
|
|
4266
|
+
}
|
|
4267
|
+
return out;
|
|
4268
|
+
}
|
|
4269
|
+
function extractBacktickPaths(planText) {
|
|
4270
|
+
const claims = [];
|
|
4271
|
+
const re = /`([^`\n]+)`/g;
|
|
4272
|
+
let m;
|
|
4273
|
+
while ((m = re.exec(planText)) !== null) {
|
|
4274
|
+
const inner = m[1].trim();
|
|
4275
|
+
if (!inner.includes("/") || inner.startsWith("http")) continue;
|
|
4276
|
+
if (inner.includes("story ") || inner.includes("Story ")) continue;
|
|
4277
|
+
const seg = inner.split(/[\s:]/)[0];
|
|
4278
|
+
if (!seg || !CODE_EXT.test(seg)) continue;
|
|
4279
|
+
claims.push({ path: seg.replace(/^\.\//, ""), isCreate: false, excerpt: m[0] });
|
|
4280
|
+
}
|
|
4281
|
+
return claims;
|
|
4282
|
+
}
|
|
4283
|
+
function extractFileHeadingPaths(planText) {
|
|
4284
|
+
const claims = [];
|
|
4285
|
+
const fileRe = /\*\*\s*(?:File|Create file)\s*:\s*`([^`\n]+)`\s*\*\*/gi;
|
|
4286
|
+
let m;
|
|
4287
|
+
while ((m = fileRe.exec(planText)) !== null) {
|
|
4288
|
+
const p = m[1].trim().replace(/^\.\//, "");
|
|
4289
|
+
const isCreate = /create file/i.test(m[0]);
|
|
4290
|
+
claims.push({ path: p, isCreate, excerpt: m[0].slice(0, 120) });
|
|
4291
|
+
}
|
|
4292
|
+
return claims;
|
|
4293
|
+
}
|
|
4294
|
+
function mergePathClaims(planText) {
|
|
4295
|
+
const map = /* @__PURE__ */ new Map();
|
|
4296
|
+
for (const c of [...extractFileHeadingPaths(planText), ...extractBacktickPaths(planText)]) {
|
|
4297
|
+
const key = `${c.isCreate}:${c.path}`;
|
|
4298
|
+
if (!map.has(key)) map.set(key, c);
|
|
4299
|
+
}
|
|
4300
|
+
return [...map.values()];
|
|
4301
|
+
}
|
|
4302
|
+
var LINE_RANGE_RE = /(?:~?\s*lines?\s*|lines\s+)(\d+)\s*[–-]\s*(\d+)/gi;
|
|
4303
|
+
var LINE_SINGLE_WORD_RE = /\bline\s+(\d+)\b|\(L(\d+)\)/gi;
|
|
4304
|
+
var PATH_COLON_LINE_RE = /`([^`\n]+\.[a-z0-9]{1,6})`\s*:\s*(\d+)\b/gi;
|
|
4305
|
+
function pathNear(planText, idx) {
|
|
4306
|
+
const before = planText.slice(Math.max(0, idx - 200), idx);
|
|
4307
|
+
const pathMatch = before.match(/`([^`\n]+\.[a-z0-9]{1,6})`/i);
|
|
4308
|
+
const filePath = pathMatch?.[1]?.trim().replace(/^\.\//, "");
|
|
4309
|
+
return filePath;
|
|
4310
|
+
}
|
|
4311
|
+
function trigrams(s) {
|
|
4312
|
+
const x = s.toLowerCase();
|
|
4313
|
+
const g = /* @__PURE__ */ new Set();
|
|
4314
|
+
for (let i = 0; i + 3 <= x.length; i++) {
|
|
4315
|
+
g.add(x.slice(i, i + 3));
|
|
4316
|
+
}
|
|
4317
|
+
return g;
|
|
4318
|
+
}
|
|
4319
|
+
function trigramSimilarity(a, b) {
|
|
4320
|
+
const A = trigrams(a);
|
|
4321
|
+
const B = trigrams(b);
|
|
4322
|
+
if (A.size === 0 || B.size === 0) return 0;
|
|
4323
|
+
let inter = 0;
|
|
4324
|
+
for (const t of A) {
|
|
4325
|
+
if (B.has(t)) inter += 1;
|
|
4326
|
+
}
|
|
4327
|
+
return inter / Math.sqrt(A.size * B.size);
|
|
4328
|
+
}
|
|
4329
|
+
function suggestPaths(root, missingRel, max = 3) {
|
|
4330
|
+
const targetBase = path18.basename(missingRel).toLowerCase();
|
|
4331
|
+
const ig = buildRepoIgnore(root);
|
|
4332
|
+
const candidates = [];
|
|
4333
|
+
const walk2 = (dir) => {
|
|
4334
|
+
if (candidates.length >= 2e3) return;
|
|
4335
|
+
let ents;
|
|
4336
|
+
try {
|
|
4337
|
+
ents = fs26.readdirSync(dir, { withFileTypes: true });
|
|
4338
|
+
} catch {
|
|
4339
|
+
return;
|
|
4340
|
+
}
|
|
4341
|
+
for (const ent of ents) {
|
|
4342
|
+
if (candidates.length >= 2e3) return;
|
|
4343
|
+
const abs = path18.join(dir, ent.name);
|
|
4344
|
+
const rel = path18.relative(root, abs).split(path18.sep).join("/");
|
|
4345
|
+
if (!rel || ig.ignores(rel) || ent.isDirectory() && ig.ignores(`${rel}/`)) continue;
|
|
4346
|
+
if (ent.isDirectory()) walk2(abs);
|
|
4347
|
+
else if (ent.isFile()) candidates.push(rel);
|
|
4348
|
+
}
|
|
4349
|
+
};
|
|
4350
|
+
walk2(root);
|
|
4351
|
+
const rows = [];
|
|
4352
|
+
for (const rel of candidates) {
|
|
4353
|
+
const base = path18.basename(rel).toLowerCase();
|
|
4354
|
+
let score = 0;
|
|
4355
|
+
if (base === targetBase) score = 1e3;
|
|
4356
|
+
else if (base.startsWith(targetBase) || targetBase.startsWith(base)) score = 500;
|
|
4357
|
+
else if (base.includes(targetBase) || targetBase.includes(base)) score = 200;
|
|
4358
|
+
score += trigramSimilarity(targetBase, base) * 50;
|
|
4359
|
+
if (score > 0) rows.push({ rel, score });
|
|
4360
|
+
}
|
|
4361
|
+
rows.sort((a, b) => b.score - a.score || a.rel.localeCompare(b.rel));
|
|
4362
|
+
const uniq = [];
|
|
4363
|
+
for (const r of rows) {
|
|
4364
|
+
if (r.rel === missingRel.replace(/^\.\//, "")) continue;
|
|
4365
|
+
if (!uniq.includes(r.rel)) uniq.push(r.rel);
|
|
4366
|
+
if (uniq.length >= max) break;
|
|
4367
|
+
}
|
|
4368
|
+
return uniq;
|
|
4369
|
+
}
|
|
4370
|
+
var SYMBOL_DEF_RE = /(?:^|\n)\s*(?:export\s+)?(?:async\s+)?(?:function|class|interface|type|const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
4371
|
+
function collectSymbols(src, maxLines = 200) {
|
|
4372
|
+
const head = src.split(/\r?\n/).slice(0, maxLines).join("\n");
|
|
4373
|
+
const syms = /* @__PURE__ */ new Set();
|
|
4374
|
+
SYMBOL_DEF_RE.lastIndex = 0;
|
|
4375
|
+
let m;
|
|
4376
|
+
while ((m = SYMBOL_DEF_RE.exec(head)) !== null) {
|
|
4377
|
+
syms.add(m[1]);
|
|
4378
|
+
}
|
|
4379
|
+
return [...syms];
|
|
4380
|
+
}
|
|
4381
|
+
function leadingCharScore(sym, missing) {
|
|
4382
|
+
let i = 0;
|
|
4383
|
+
const a = sym.toLowerCase();
|
|
4384
|
+
const b = missing.toLowerCase();
|
|
4385
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i++;
|
|
4386
|
+
return i * 10 + trigramSimilarity(sym, missing) * 30;
|
|
4387
|
+
}
|
|
4388
|
+
function suggestSymbols(fileText, missingSym, max = 3) {
|
|
4389
|
+
const defs = collectSymbols(fileText);
|
|
4390
|
+
const ranked = defs.filter((s) => s !== missingSym).map((s) => ({ s, score: leadingCharScore(s, missingSym) })).filter((r) => r.score > 0).sort((a, b) => b.score - a.score || a.s.localeCompare(b.s));
|
|
4391
|
+
const out = [];
|
|
4392
|
+
for (const r of ranked) {
|
|
4393
|
+
if (!out.includes(r.s)) out.push(r.s);
|
|
4394
|
+
if (out.length >= max) break;
|
|
4395
|
+
}
|
|
4396
|
+
return out;
|
|
4397
|
+
}
|
|
4398
|
+
function parseMetadataLine(planText) {
|
|
4399
|
+
const lines = planText.split(/\r?\n/);
|
|
4400
|
+
for (let i = 0; i < Math.min(5, lines.length); i++) {
|
|
4401
|
+
const line2 = (lines[i] ?? "").trim();
|
|
4402
|
+
if (!line2.startsWith("<!--") || !line2.includes("squad-kit:")) continue;
|
|
4403
|
+
if (!line2.endsWith("-->")) {
|
|
4404
|
+
return {
|
|
4405
|
+
severity: "warning",
|
|
4406
|
+
kind: "malformed_metadata",
|
|
4407
|
+
detail: "Metadata comment is not a single-line <!-- squad-kit: \u2026 -->",
|
|
4408
|
+
excerpt: line2.slice(0, 120)
|
|
4409
|
+
};
|
|
4410
|
+
}
|
|
4411
|
+
const inner = line2.replace(/^<!--\s*squad-kit:\s*/i, "").replace(/\s*-->$/, "");
|
|
4412
|
+
if (!inner.trim()) {
|
|
4413
|
+
return {
|
|
4414
|
+
severity: "warning",
|
|
4415
|
+
kind: "malformed_metadata",
|
|
4416
|
+
detail: "Empty squad-kit metadata body",
|
|
4417
|
+
excerpt: line2
|
|
4418
|
+
};
|
|
4419
|
+
}
|
|
4420
|
+
return void 0;
|
|
4421
|
+
}
|
|
4422
|
+
return void 0;
|
|
4423
|
+
}
|
|
4424
|
+
function validatePlan(input5) {
|
|
4425
|
+
const issues = [];
|
|
4426
|
+
const metaIssue = parseMetadataLine(input5.planText);
|
|
4427
|
+
if (metaIssue) issues.push(metaIssue);
|
|
4428
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4429
|
+
const readCached = (rel) => {
|
|
4430
|
+
if (cache.has(rel)) return cache.get(rel);
|
|
4431
|
+
const resolved = path18.resolve(input5.root, rel);
|
|
4432
|
+
const rc = path18.relative(input5.root, resolved);
|
|
4433
|
+
if (rc.startsWith("..") || path18.isAbsolute(rc)) return void 0;
|
|
4434
|
+
if (!fs26.existsSync(resolved) || !fs26.statSync(resolved).isFile()) return void 0;
|
|
4435
|
+
try {
|
|
4436
|
+
const buf = fs26.readFileSync(resolved);
|
|
4437
|
+
const isBinary = buf.includes(0);
|
|
4438
|
+
const text = isBinary ? "" : buf.toString("utf8");
|
|
4439
|
+
const lines = isBinary ? 0 : text.split(/\r?\n/).length;
|
|
4440
|
+
const entry = { text, lines, isBinary };
|
|
4441
|
+
cache.set(rel, entry);
|
|
4442
|
+
return entry;
|
|
4443
|
+
} catch {
|
|
4444
|
+
return void 0;
|
|
4445
|
+
}
|
|
4446
|
+
};
|
|
4447
|
+
const pathClaims = mergePathClaims(input5.planText);
|
|
4448
|
+
for (const c of pathClaims) {
|
|
4449
|
+
if (c.isCreate) continue;
|
|
4450
|
+
const resolved = path18.resolve(input5.root, c.path);
|
|
4451
|
+
const rel = path18.relative(input5.root, resolved);
|
|
4452
|
+
if (rel.startsWith("..") || path18.isAbsolute(rel)) continue;
|
|
4453
|
+
if (!fs26.existsSync(resolved)) {
|
|
4454
|
+
const suggestions = suggestPaths(input5.root, c.path);
|
|
4455
|
+
issues.push({
|
|
4456
|
+
severity: "warning",
|
|
4457
|
+
kind: "missing_path",
|
|
4458
|
+
path: c.path,
|
|
4459
|
+
detail: "Path cited in plan does not exist in the workspace" + (suggestions.length ? ` \u2014 did you mean: ${suggestions.join(", ")}?` : ""),
|
|
4460
|
+
excerpt: c.excerpt
|
|
4461
|
+
});
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
const rangeText = input5.planText;
|
|
4465
|
+
LINE_RANGE_RE.lastIndex = 0;
|
|
4466
|
+
let rm2;
|
|
4467
|
+
while ((rm2 = LINE_RANGE_RE.exec(rangeText)) !== null) {
|
|
4468
|
+
const lo = parseInt(rm2[1], 10);
|
|
4469
|
+
const hi = parseInt(rm2[2], 10);
|
|
4470
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi)) continue;
|
|
4471
|
+
const filePath = pathNear(rangeText, rm2.index);
|
|
4472
|
+
if (!filePath) continue;
|
|
4473
|
+
const cached = readCached(filePath);
|
|
4474
|
+
const lines = cached?.isBinary ? void 0 : cached?.lines;
|
|
4475
|
+
if (lines !== void 0 && hi > lines) {
|
|
4476
|
+
issues.push({
|
|
4477
|
+
severity: "warning",
|
|
4478
|
+
kind: "line_range_too_large",
|
|
4479
|
+
path: filePath,
|
|
4480
|
+
detail: `Range ends at line ${hi} but file has ${lines} lines`,
|
|
4481
|
+
excerpt: rm2[0].slice(0, 80)
|
|
4482
|
+
});
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
LINE_SINGLE_WORD_RE.lastIndex = 0;
|
|
4486
|
+
while ((rm2 = LINE_SINGLE_WORD_RE.exec(rangeText)) !== null) {
|
|
4487
|
+
const n = parseInt(rm2[1] || rm2[2] || "", 10);
|
|
4488
|
+
if (!Number.isFinite(n)) continue;
|
|
4489
|
+
const filePath = pathNear(rangeText, rm2.index);
|
|
4490
|
+
if (!filePath) continue;
|
|
4491
|
+
const cached = readCached(filePath);
|
|
4492
|
+
const lines = cached?.isBinary ? void 0 : cached?.lines;
|
|
4493
|
+
if (lines !== void 0 && n > lines) {
|
|
4494
|
+
issues.push({
|
|
4495
|
+
severity: "warning",
|
|
4496
|
+
kind: "line_range_too_large",
|
|
4497
|
+
path: filePath,
|
|
4498
|
+
detail: `Range ends at line ${n} but file has ${lines} lines`,
|
|
4499
|
+
excerpt: rm2[0].slice(0, 80)
|
|
4500
|
+
});
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
PATH_COLON_LINE_RE.lastIndex = 0;
|
|
4504
|
+
while ((rm2 = PATH_COLON_LINE_RE.exec(rangeText)) !== null) {
|
|
4505
|
+
const fp = rm2[1].trim().replace(/^\.\//, "");
|
|
4506
|
+
const n = parseInt(rm2[2], 10);
|
|
4507
|
+
if (!Number.isFinite(n)) continue;
|
|
4508
|
+
const cached = readCached(fp);
|
|
4509
|
+
const lines = cached?.isBinary ? void 0 : cached?.lines;
|
|
4510
|
+
if (lines !== void 0 && n > lines) {
|
|
4511
|
+
issues.push({
|
|
4512
|
+
severity: "warning",
|
|
4513
|
+
kind: "line_range_too_large",
|
|
4514
|
+
path: fp,
|
|
4515
|
+
detail: `Range ends at line ${n} but file has ${lines} lines`,
|
|
4516
|
+
excerpt: rm2[0].slice(0, 80)
|
|
4517
|
+
});
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
const textByLine = input5.planText.split(/\r?\n/);
|
|
4521
|
+
for (let li = 0; li < textByLine.length; li++) {
|
|
4522
|
+
const line2 = textByLine[li] ?? "";
|
|
4523
|
+
const near = [textByLine[li - 1] ?? "", line2, textByLine[li + 1] ?? ""].join("\n");
|
|
4524
|
+
const pathNearCell = near.match(/`([^`\n/]+\/[^`\n]+?\.[a-z0-9]{1,6})`/i);
|
|
4525
|
+
if (!pathNearCell) continue;
|
|
4526
|
+
const fp = pathNearCell[1].replace(/^\.\//, "");
|
|
4527
|
+
const cached = readCached(fp);
|
|
4528
|
+
if (!cached || cached.isBinary) continue;
|
|
4529
|
+
const symRe = /`([A-Za-z_][A-Za-z0-9_]{2,50})`/g;
|
|
4530
|
+
let sm;
|
|
4531
|
+
while ((sm = symRe.exec(line2)) !== null) {
|
|
4532
|
+
const sym = sm[1];
|
|
4533
|
+
if (["true", "false", "null", "undefined", "string", "number", "boolean", "void", "never"].includes(
|
|
4534
|
+
sym
|
|
4535
|
+
)) {
|
|
4536
|
+
continue;
|
|
4537
|
+
}
|
|
4538
|
+
if (!cached.text.includes(sym)) {
|
|
4539
|
+
const suggestions = suggestSymbols(cached.text, sym);
|
|
4540
|
+
issues.push({
|
|
4541
|
+
severity: "warning",
|
|
4542
|
+
kind: "symbol_not_found",
|
|
4543
|
+
path: fp,
|
|
4544
|
+
detail: `Symbol \`${sym}\` not found as substring in file` + (suggestions.length ? ` \u2014 did you mean: ${suggestions.map((s) => `\`${s}\``).join(", ")}?` : ""),
|
|
4545
|
+
excerpt: sm[0]
|
|
4546
|
+
});
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
return issues;
|
|
4551
|
+
}
|
|
4552
|
+
|
|
3869
4553
|
// src/planner/loop.ts
|
|
3870
4554
|
var MAX_RATE_LIMIT_RETRY_SEC = 90;
|
|
3871
4555
|
var defaultSleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
@@ -3903,8 +4587,147 @@ function buildPlannerRunStats(budget, turns, runStartedAt) {
|
|
|
3903
4587
|
durationMs: Date.now() - runStartedAt
|
|
3904
4588
|
};
|
|
3905
4589
|
}
|
|
3906
|
-
function
|
|
3907
|
-
|
|
4590
|
+
function buildScoutedContextSection(root, budget, selectedFiles, bus, runId, readRanges) {
|
|
4591
|
+
const normalizeRel = (p) => p.replace(/^\.\//, "");
|
|
4592
|
+
const rangeByPath = /* @__PURE__ */ new Map();
|
|
4593
|
+
for (const rr of readRanges ?? []) {
|
|
4594
|
+
rangeByPath.set(normalizeRel(rr.path), rr);
|
|
4595
|
+
}
|
|
4596
|
+
const ordered = [];
|
|
4597
|
+
const seenPath = /* @__PURE__ */ new Set();
|
|
4598
|
+
for (const f of selectedFiles) {
|
|
4599
|
+
const c = normalizeRel(f);
|
|
4600
|
+
if (!seenPath.has(c)) {
|
|
4601
|
+
seenPath.add(c);
|
|
4602
|
+
ordered.push(c);
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
for (const p of rangeByPath.keys()) {
|
|
4606
|
+
if (!seenPath.has(p)) {
|
|
4607
|
+
seenPath.add(p);
|
|
4608
|
+
ordered.push(p);
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
const blocks = ["\n## Scouted context (already loaded)\n"];
|
|
4612
|
+
for (const cleaned of ordered) {
|
|
4613
|
+
const rr = rangeByPath.get(cleaned);
|
|
4614
|
+
if (rr) {
|
|
4615
|
+
const result = readFileTool(root, budget, { path: cleaned, offset: rr.offset, limit: rr.limit });
|
|
4616
|
+
const hi = rr.offset + rr.limit - 1;
|
|
4617
|
+
if (result.isError) {
|
|
4618
|
+
blocks.push(
|
|
4619
|
+
`
|
|
4620
|
+
### \`${cleaned}\` (lines ${rr.offset}\u2013${hi})
|
|
4621
|
+
|
|
4622
|
+
_${result.content}_
|
|
4623
|
+
`
|
|
4624
|
+
);
|
|
4625
|
+
} else {
|
|
4626
|
+
blocks.push(
|
|
4627
|
+
`
|
|
4628
|
+
### \`${cleaned}\` (lines ${rr.offset}\u2013${hi})
|
|
4629
|
+
|
|
4630
|
+
\`\`\`
|
|
4631
|
+
${result.content}
|
|
4632
|
+
\`\`\`
|
|
4633
|
+
`
|
|
4634
|
+
);
|
|
4635
|
+
}
|
|
4636
|
+
continue;
|
|
4637
|
+
}
|
|
4638
|
+
const resolved = path19.resolve(root, cleaned);
|
|
4639
|
+
const rc = path19.relative(root, resolved);
|
|
4640
|
+
if (rc.startsWith("..") || path19.isAbsolute(rc)) {
|
|
4641
|
+
bus.emit({
|
|
4642
|
+
kind: "validation_issue",
|
|
4643
|
+
runId,
|
|
4644
|
+
severity: "warning",
|
|
4645
|
+
issueKind: "missing_path",
|
|
4646
|
+
path: cleaned,
|
|
4647
|
+
detail: "Scout-selected path escapes the project root"
|
|
4648
|
+
});
|
|
4649
|
+
continue;
|
|
4650
|
+
}
|
|
4651
|
+
if (!fs27.existsSync(resolved)) {
|
|
4652
|
+
bus.emit({
|
|
4653
|
+
kind: "validation_issue",
|
|
4654
|
+
runId,
|
|
4655
|
+
severity: "warning",
|
|
4656
|
+
issueKind: "missing_path",
|
|
4657
|
+
path: cleaned,
|
|
4658
|
+
detail: "Scout selected this path but it does not exist on disk"
|
|
4659
|
+
});
|
|
4660
|
+
continue;
|
|
4661
|
+
}
|
|
4662
|
+
const st = fs27.statSync(resolved);
|
|
4663
|
+
if (!st.isFile()) continue;
|
|
4664
|
+
const n = Math.min(st.size, MAX_BYTES_PER_FULL_READ);
|
|
4665
|
+
const cap = budget.canRead(n);
|
|
4666
|
+
if (!cap.ok) {
|
|
4667
|
+
blocks.push(`
|
|
4668
|
+
### \`${cleaned}\`
|
|
4669
|
+
|
|
4670
|
+
_(skipped: ${cap.reason})_
|
|
4671
|
+
`);
|
|
4672
|
+
continue;
|
|
4673
|
+
}
|
|
4674
|
+
let buf;
|
|
4675
|
+
try {
|
|
4676
|
+
buf = fs27.readFileSync(resolved);
|
|
4677
|
+
} catch {
|
|
4678
|
+
continue;
|
|
4679
|
+
}
|
|
4680
|
+
if (buf.length > MAX_BYTES_PER_FULL_READ) {
|
|
4681
|
+
const head = buf.subarray(0, MAX_BYTES_PER_FULL_READ);
|
|
4682
|
+
if (looksBinary(head)) {
|
|
4683
|
+
blocks.push(`
|
|
4684
|
+
### \`${cleaned}\`
|
|
4685
|
+
|
|
4686
|
+
_(binary file skipped)_
|
|
4687
|
+
`);
|
|
4688
|
+
continue;
|
|
4689
|
+
}
|
|
4690
|
+
budget.recordRead(head.length);
|
|
4691
|
+
blocks.push(
|
|
4692
|
+
`
|
|
4693
|
+
### \`${cleaned}\` (truncated: first ${MAX_BYTES_PER_FULL_READ}-byte head; full size ${buf.length} bytes)
|
|
4694
|
+
|
|
4695
|
+
\`\`\`
|
|
4696
|
+
${head.toString("utf8")}
|
|
4697
|
+
\`\`\`
|
|
4698
|
+
`
|
|
4699
|
+
);
|
|
4700
|
+
continue;
|
|
4701
|
+
}
|
|
4702
|
+
if (looksBinary(buf)) {
|
|
4703
|
+
blocks.push(`
|
|
4704
|
+
### \`${cleaned}\`
|
|
4705
|
+
|
|
4706
|
+
_(binary file skipped)_
|
|
4707
|
+
`);
|
|
4708
|
+
continue;
|
|
4709
|
+
}
|
|
4710
|
+
budget.recordRead(buf.length);
|
|
4711
|
+
blocks.push(`
|
|
4712
|
+
### \`${cleaned}\`
|
|
4713
|
+
|
|
4714
|
+
\`\`\`
|
|
4715
|
+
${buf.toString("utf8")}
|
|
4716
|
+
\`\`\`
|
|
4717
|
+
`);
|
|
4718
|
+
}
|
|
4719
|
+
return blocks.join("");
|
|
4720
|
+
}
|
|
4721
|
+
function scoutStageStats(usage, durationMs) {
|
|
4722
|
+
return {
|
|
4723
|
+
turns: 0,
|
|
4724
|
+
inputTokens: usage.inputTokens,
|
|
4725
|
+
outputTokens: usage.outputTokens,
|
|
4726
|
+
cacheCreationTokens: 0,
|
|
4727
|
+
cacheReadTokens: 0,
|
|
4728
|
+
cacheHitRatio: 0,
|
|
4729
|
+
durationMs
|
|
4730
|
+
};
|
|
3908
4731
|
}
|
|
3909
4732
|
function extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut) {
|
|
3910
4733
|
input5.budget.extendSession();
|
|
@@ -3944,28 +4767,106 @@ function cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, i
|
|
|
3944
4767
|
userCancelled: true
|
|
3945
4768
|
};
|
|
3946
4769
|
}
|
|
3947
|
-
|
|
4770
|
+
function asToolInput(args) {
|
|
4771
|
+
if (args && typeof args === "object" && !Array.isArray(args)) return args;
|
|
4772
|
+
if (typeof args === "string") {
|
|
4773
|
+
try {
|
|
4774
|
+
return JSON.parse(args);
|
|
4775
|
+
} catch {
|
|
4776
|
+
return {};
|
|
4777
|
+
}
|
|
4778
|
+
}
|
|
4779
|
+
return {};
|
|
4780
|
+
}
|
|
4781
|
+
function normalizeToolExecutionResult(result) {
|
|
4782
|
+
if (typeof result === "string") {
|
|
4783
|
+
try {
|
|
4784
|
+
const j = JSON.parse(result);
|
|
4785
|
+
if (j && typeof j === "object" && !Array.isArray(j) && "content" in j) {
|
|
4786
|
+
const o = j;
|
|
4787
|
+
return {
|
|
4788
|
+
content: typeof o.content === "string" ? o.content : JSON.stringify(o.content ?? ""),
|
|
4789
|
+
isError: o.isError === true
|
|
4790
|
+
};
|
|
4791
|
+
}
|
|
4792
|
+
} catch {
|
|
4793
|
+
}
|
|
4794
|
+
return { content: result, isError: false };
|
|
4795
|
+
}
|
|
4796
|
+
if (result && typeof result === "object" && !Array.isArray(result) && "content" in result) {
|
|
4797
|
+
const o = result;
|
|
4798
|
+
return {
|
|
4799
|
+
content: typeof o.content === "string" ? o.content : JSON.stringify(o.content ?? ""),
|
|
4800
|
+
isError: o.isError === true
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
return { content: typeof result === "undefined" ? "" : JSON.stringify(result), isError: false };
|
|
4804
|
+
}
|
|
4805
|
+
function applyStepToTurns(turns, step2) {
|
|
4806
|
+
const callsById = /* @__PURE__ */ new Map();
|
|
4807
|
+
const toolCalls = step2.toolCalls.map((tc) => {
|
|
4808
|
+
callsById.set(tc.toolCallId, tc.toolName);
|
|
4809
|
+
return { id: tc.toolCallId, name: tc.toolName, input: asToolInput(tc.args) };
|
|
4810
|
+
});
|
|
4811
|
+
turns.push({
|
|
4812
|
+
role: "assistant",
|
|
4813
|
+
...step2.text ? { text: step2.text } : {},
|
|
4814
|
+
...toolCalls.length ? { toolCalls } : {}
|
|
4815
|
+
});
|
|
4816
|
+
if (step2.toolResults.length) {
|
|
4817
|
+
const toolResults = step2.toolResults.map((tr) => {
|
|
4818
|
+
const norm = normalizeToolExecutionResult(tr.result);
|
|
4819
|
+
return {
|
|
4820
|
+
toolCallId: tr.toolCallId,
|
|
4821
|
+
name: callsById.get(tr.toolCallId) ?? "read_file",
|
|
4822
|
+
content: norm.content,
|
|
4823
|
+
...norm.isError ? { isError: true } : {}
|
|
4824
|
+
};
|
|
4825
|
+
});
|
|
4826
|
+
turns.push({ role: "user", toolResults });
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
function providerResponseFromApiError(provider, modelId, err2) {
|
|
4830
|
+
const body = err2.responseBody ?? "";
|
|
4831
|
+
const nf = detectModelNotFound(provider, modelId, err2.statusCode ?? 0, body);
|
|
4832
|
+
if (nf) {
|
|
4833
|
+
return {
|
|
4834
|
+
stopReason: "error",
|
|
4835
|
+
errorKind: "model_not_found",
|
|
4836
|
+
rawError: modelNotFoundMessage(nf)
|
|
4837
|
+
};
|
|
4838
|
+
}
|
|
4839
|
+
const rl = detectRateLimit(provider, err2.statusCode ?? 0, err2.responseHeaders ?? {}, body);
|
|
4840
|
+
if (rl) {
|
|
4841
|
+
return {
|
|
4842
|
+
stopReason: "error",
|
|
4843
|
+
errorKind: "rate_limit",
|
|
4844
|
+
retryAfterSec: rl.retryAfterSec,
|
|
4845
|
+
rawError: `${provider} ${err2.statusCode ?? "error"}: ${body.slice(0, 500)}`
|
|
4846
|
+
};
|
|
4847
|
+
}
|
|
4848
|
+
return {
|
|
4849
|
+
stopReason: "error",
|
|
4850
|
+
rawError: `${provider} ${err2.statusCode ?? "error"}: ${body.slice(0, 500)}`
|
|
4851
|
+
};
|
|
4852
|
+
}
|
|
4853
|
+
async function runDraftStage(input5) {
|
|
3948
4854
|
const runStartedAt = Date.now();
|
|
3949
4855
|
const bus = input5.events ?? new PlannerEventBus();
|
|
3950
4856
|
const runId = input5.runId ?? newRunId();
|
|
3951
4857
|
const cacheEnabled = input5.cacheEnabled ?? true;
|
|
3952
|
-
bus.emit({
|
|
3953
|
-
kind: "started",
|
|
3954
|
-
runId,
|
|
3955
|
-
provider: input5.provider.name,
|
|
3956
|
-
model: input5.model,
|
|
3957
|
-
cacheEnabled
|
|
3958
|
-
});
|
|
3959
4858
|
const baseMaxIter = input5.maxIterations ?? DEFAULT_PLANNER_MAX_ITERATIONS;
|
|
3960
4859
|
const maxIter = { current: baseMaxIter };
|
|
3961
4860
|
const baseMaxOut = input5.maxOutputTokens ?? DEFAULT_PLANNER_MAX_OUTPUT_TOKENS;
|
|
3962
4861
|
const maxOut = { current: baseMaxOut };
|
|
3963
4862
|
const turns = [{ role: "user", text: input5.userPrompt }];
|
|
4863
|
+
if (input5.scoutedSection?.trim()) {
|
|
4864
|
+
turns.push({ role: "user", text: composeScoutedUserMessage({ scoutedSection: input5.scoutedSection }) });
|
|
4865
|
+
}
|
|
3964
4866
|
let accumulatedText = "";
|
|
3965
4867
|
let iterations = 0;
|
|
3966
4868
|
let budgetExhausted = false;
|
|
3967
4869
|
let finishedNormally = false;
|
|
3968
|
-
let prevRequestPrefix;
|
|
3969
4870
|
const decide = input5.decideOnLimit;
|
|
3970
4871
|
const sleepFn = input5.sleep ?? defaultSleep;
|
|
3971
4872
|
const limitCtx = (kind) => ({
|
|
@@ -4036,112 +4937,160 @@ async function runPlanner(input5) {
|
|
|
4036
4937
|
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
4037
4938
|
};
|
|
4038
4939
|
}
|
|
4039
|
-
if (input5.budget.overCost()) {
|
|
4040
|
-
if (decide) {
|
|
4041
|
-
const d = await decide(limitCtx("cost_cap"));
|
|
4042
|
-
if (d === "cancel") {
|
|
4043
|
-
return {
|
|
4044
|
-
planText: accumulatedText,
|
|
4045
|
-
budgetExhausted,
|
|
4046
|
-
timedOut: false,
|
|
4047
|
-
finishedNormally: false,
|
|
4048
|
-
iterations,
|
|
4049
|
-
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
|
|
4050
|
-
incompleteKind: "cost_cap",
|
|
4051
|
-
userCancelled: true
|
|
4052
|
-
};
|
|
4053
|
-
}
|
|
4054
|
-
extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut);
|
|
4055
|
-
iterations -= 1;
|
|
4056
|
-
continue;
|
|
4057
|
-
}
|
|
4058
|
-
return {
|
|
4059
|
-
planText: accumulatedText,
|
|
4060
|
-
budgetExhausted: true,
|
|
4061
|
-
timedOut: false,
|
|
4062
|
-
finishedNormally: false,
|
|
4063
|
-
iterations,
|
|
4064
|
-
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
4065
|
-
};
|
|
4066
|
-
}
|
|
4067
4940
|
if (iterations >= 2) {
|
|
4068
4941
|
bus.emit({ kind: "turn_started", runId, turn: iterations });
|
|
4069
4942
|
}
|
|
4070
|
-
const
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4943
|
+
const tools = buildPlannerTools({
|
|
4944
|
+
root: input5.root,
|
|
4945
|
+
budget: input5.budget,
|
|
4946
|
+
enabled: {
|
|
4947
|
+
grep: input5.toolsEnabled?.grep ?? true,
|
|
4948
|
+
listDir: input5.toolsEnabled?.listDir ?? true,
|
|
4949
|
+
rangedRead: input5.toolsEnabled?.rangedRead ?? true
|
|
4950
|
+
},
|
|
4951
|
+
readHooks: {
|
|
4952
|
+
runId,
|
|
4953
|
+
turn: iterations,
|
|
4954
|
+
bus,
|
|
4955
|
+
onToolCall: input5.onToolCall,
|
|
4956
|
+
decideOnLimit: input5.decideOnLimit,
|
|
4957
|
+
getLimitCtx: () => limitCtx("file_or_context_reads"),
|
|
4958
|
+
extendSessionLimits: () => extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut),
|
|
4959
|
+
getAccumulatedText: () => accumulatedText,
|
|
4960
|
+
setBudgetExhausted: (v) => {
|
|
4961
|
+
budgetExhausted = v;
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
});
|
|
4965
|
+
const messages = turnsToCoreMessages(input5.systemPrompt, turns, {
|
|
4077
4966
|
cacheEnabled,
|
|
4078
|
-
|
|
4079
|
-
};
|
|
4967
|
+
provider: input5.provider
|
|
4968
|
+
});
|
|
4080
4969
|
bus.emit({ kind: "request_sent", runId, turn: iterations });
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
if (!currentPrefix.startsWith(prevRequestPrefix)) {
|
|
4084
|
-
const byte = firstByteDiff(prevRequestPrefix, currentPrefix);
|
|
4085
|
-
warning(
|
|
4086
|
-
`planner: prefix mutation at turn ${iterations}, byte ${byte} \u2014 caching will not hit.`
|
|
4087
|
-
);
|
|
4088
|
-
}
|
|
4089
|
-
}
|
|
4090
|
-
let response;
|
|
4091
|
-
try {
|
|
4092
|
-
response = await input5.provider.send(request);
|
|
4093
|
-
} catch (e) {
|
|
4094
|
-
if (isAbortError(e)) {
|
|
4095
|
-
return cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, iterations, runStartedAt);
|
|
4096
|
-
}
|
|
4097
|
-
throw e;
|
|
4098
|
-
}
|
|
4099
|
-
if (response.usage) {
|
|
4100
|
-
recordUsageAndEmit(bus, input5, runId, iterations, response.usage);
|
|
4101
|
-
}
|
|
4102
|
-
let retriedRateLimit = false;
|
|
4970
|
+
let attempt = 0;
|
|
4971
|
+
let stepResult;
|
|
4103
4972
|
let retrySkippedReason;
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4973
|
+
let retriedRateLimit = false;
|
|
4974
|
+
const callStream = async () => {
|
|
4975
|
+
const { streamText } = await import("ai");
|
|
4976
|
+
const stream = streamText({
|
|
4977
|
+
model: input5.model,
|
|
4978
|
+
messages,
|
|
4979
|
+
tools,
|
|
4980
|
+
toolChoice: "auto",
|
|
4981
|
+
maxSteps: 1,
|
|
4982
|
+
maxRetries: 0,
|
|
4983
|
+
maxTokens: maxOut.current,
|
|
4984
|
+
abortSignal: input5.abort,
|
|
4985
|
+
onChunk: ({ chunk }) => {
|
|
4986
|
+
if (chunk.type === "text-delta") {
|
|
4987
|
+
accumulatedText += chunk.textDelta;
|
|
4988
|
+
input5.onAssistantText?.(chunk.textDelta);
|
|
4989
|
+
bus.emit({ kind: "assistant_text", runId, turn: iterations, delta: chunk.textDelta });
|
|
4117
4990
|
}
|
|
4118
|
-
|
|
4991
|
+
},
|
|
4992
|
+
onStepFinish: (step2) => {
|
|
4993
|
+
const u = usageFromLanguageModelStep(step2.usage, step2.providerMetadata);
|
|
4994
|
+
recordUsageAndEmit(bus, input5, runId, iterations, u);
|
|
4119
4995
|
}
|
|
4996
|
+
});
|
|
4997
|
+
for await (const part of stream.fullStream) {
|
|
4998
|
+
const p = part;
|
|
4999
|
+
if (p.type === "error") {
|
|
5000
|
+
throw p.error;
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
const steps = await stream.steps;
|
|
5004
|
+
const last = steps[steps.length - 1];
|
|
5005
|
+
if (!last) throw new Error("planner: empty model step");
|
|
5006
|
+
stepResult = {
|
|
5007
|
+
text: last.text,
|
|
5008
|
+
finishReason: last.finishReason,
|
|
5009
|
+
toolCalls: last.toolCalls.flatMap(
|
|
5010
|
+
(tc) => tc ? [
|
|
5011
|
+
{
|
|
5012
|
+
toolCallId: tc.toolCallId,
|
|
5013
|
+
toolName: tc.toolName,
|
|
5014
|
+
args: tc.args
|
|
5015
|
+
}
|
|
5016
|
+
] : []
|
|
5017
|
+
),
|
|
5018
|
+
toolResults: last.toolResults.flatMap(
|
|
5019
|
+
(tr) => tr ? [{ toolCallId: tr.toolCallId, result: tr.result }] : []
|
|
5020
|
+
),
|
|
5021
|
+
usage: last.usage,
|
|
5022
|
+
providerMetadata: last.providerMetadata
|
|
5023
|
+
};
|
|
5024
|
+
};
|
|
5025
|
+
try {
|
|
5026
|
+
while (true) {
|
|
4120
5027
|
try {
|
|
4121
|
-
|
|
5028
|
+
await callStream();
|
|
5029
|
+
break;
|
|
4122
5030
|
} catch (e) {
|
|
5031
|
+
if (PlannerUserCancelledError.is(e)) {
|
|
5032
|
+
return {
|
|
5033
|
+
planText: e.planText,
|
|
5034
|
+
budgetExhausted: false,
|
|
5035
|
+
timedOut: false,
|
|
5036
|
+
finishedNormally: false,
|
|
5037
|
+
iterations,
|
|
5038
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
|
|
5039
|
+
incompleteKind: "budget_reads",
|
|
5040
|
+
userCancelled: true
|
|
5041
|
+
};
|
|
5042
|
+
}
|
|
4123
5043
|
if (isAbortError(e)) {
|
|
4124
5044
|
return cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, iterations, runStartedAt);
|
|
4125
5045
|
}
|
|
5046
|
+
if (APICallError2.isInstance(e)) {
|
|
5047
|
+
const synthetic = providerResponseFromApiError(input5.provider, input5.modelId, e);
|
|
5048
|
+
if (synthetic.errorKind === "rate_limit") {
|
|
5049
|
+
const asked = synthetic.retryAfterSec;
|
|
5050
|
+
const emitRateLimit = (phase, waitSec) => {
|
|
5051
|
+
bus.emit({
|
|
5052
|
+
kind: "rate_limit",
|
|
5053
|
+
runId,
|
|
5054
|
+
turn: iterations,
|
|
5055
|
+
retryAfterSec: synthetic.retryAfterSec,
|
|
5056
|
+
waitSec,
|
|
5057
|
+
capSec: MAX_RATE_LIMIT_RETRY_SEC,
|
|
5058
|
+
phase,
|
|
5059
|
+
provider: input5.provider,
|
|
5060
|
+
rawBody: (synthetic.rawError ?? "").slice(0, 200)
|
|
5061
|
+
});
|
|
5062
|
+
};
|
|
5063
|
+
if (asked !== void 0 && asked > MAX_RATE_LIMIT_RETRY_SEC) {
|
|
5064
|
+
emitRateLimit("aborted", asked);
|
|
5065
|
+
retrySkippedReason = "retry_after_too_long";
|
|
5066
|
+
throw composePlannerError(input5.provider, synthetic, false, retrySkippedReason);
|
|
5067
|
+
}
|
|
5068
|
+
if (attempt === 0) {
|
|
5069
|
+
const waitSec = Math.min(asked ?? 10, MAX_RATE_LIMIT_RETRY_SEC);
|
|
5070
|
+
emitRateLimit("retrying", waitSec);
|
|
5071
|
+
input5.onRateLimit?.(waitSec);
|
|
5072
|
+
await sleepWithAbort(waitSec * 1e3, sleepFn, input5.abort);
|
|
5073
|
+
attempt += 1;
|
|
5074
|
+
retriedRateLimit = true;
|
|
5075
|
+
continue;
|
|
5076
|
+
}
|
|
5077
|
+
throw composePlannerError(input5.provider, synthetic, true, retrySkippedReason);
|
|
5078
|
+
}
|
|
5079
|
+
throw composePlannerError(input5.provider, synthetic, retriedRateLimit, retrySkippedReason);
|
|
5080
|
+
}
|
|
4126
5081
|
throw e;
|
|
4127
5082
|
}
|
|
4128
|
-
if (response.usage) {
|
|
4129
|
-
recordUsageAndEmit(bus, input5, runId, iterations, response.usage);
|
|
4130
|
-
}
|
|
4131
|
-
retriedRateLimit = true;
|
|
4132
5083
|
}
|
|
5084
|
+
} catch (e) {
|
|
5085
|
+
if (isAbortError(e)) {
|
|
5086
|
+
return cancelledOutput(input5, bus, runId, accumulatedText, budgetExhausted, iterations, runStartedAt);
|
|
5087
|
+
}
|
|
5088
|
+
throw e;
|
|
4133
5089
|
}
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
if (response.text) {
|
|
4139
|
-
accumulatedText += response.text;
|
|
4140
|
-
input5.onAssistantText?.(response.text);
|
|
4141
|
-
bus.emit({ kind: "assistant_text", runId, turn: iterations, delta: response.text });
|
|
4142
|
-
}
|
|
4143
|
-
if (response.stopReason === "max_tokens" && !response.toolCalls?.length) {
|
|
4144
|
-
bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: response.stopReason });
|
|
5090
|
+
const finishReason = stepResult.finishReason;
|
|
5091
|
+
const toolCallsLen = stepResult.toolCalls.length;
|
|
5092
|
+
if (finishReason === "length" && toolCallsLen === 0) {
|
|
5093
|
+
bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: "max_tokens" });
|
|
4145
5094
|
if (decide) {
|
|
4146
5095
|
const d = await decide(limitCtx("max_output_tokens"));
|
|
4147
5096
|
if (d === "cancel") {
|
|
@@ -4157,7 +5106,7 @@ async function runPlanner(input5) {
|
|
|
4157
5106
|
};
|
|
4158
5107
|
}
|
|
4159
5108
|
extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut);
|
|
4160
|
-
turns
|
|
5109
|
+
applyStepToTurns(turns, stepResult);
|
|
4161
5110
|
turns.push({ role: "user", text: PLANNER_MARKDOWN_CONTINUATION_USER });
|
|
4162
5111
|
iterations -= 1;
|
|
4163
5112
|
continue;
|
|
@@ -4172,8 +5121,13 @@ async function runPlanner(input5) {
|
|
|
4172
5121
|
incompleteKind: "max_output_tokens"
|
|
4173
5122
|
};
|
|
4174
5123
|
}
|
|
4175
|
-
if (
|
|
4176
|
-
|
|
5124
|
+
if (finishReason === "error" || finishReason === "content-filter") {
|
|
5125
|
+
throw new Error(
|
|
5126
|
+
stepResult.text || `planner: model reported ${finishReason}. Run \`squad doctor\` to diagnose, or retry \u2014 most 5xx errors are transient.`
|
|
5127
|
+
);
|
|
5128
|
+
}
|
|
5129
|
+
if ((finishReason === "stop" || finishReason === "unknown") && toolCallsLen === 0) {
|
|
5130
|
+
bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
|
|
4177
5131
|
finishedNormally = true;
|
|
4178
5132
|
return {
|
|
4179
5133
|
planText: accumulatedText,
|
|
@@ -4184,81 +5138,173 @@ async function runPlanner(input5) {
|
|
|
4184
5138
|
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
4185
5139
|
};
|
|
4186
5140
|
}
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
input5.onToolCall?.(tc, bytesLoaded, after);
|
|
4205
|
-
bus.emit({
|
|
4206
|
-
kind: "tool_call",
|
|
4207
|
-
runId,
|
|
4208
|
-
turn: iterations,
|
|
4209
|
-
toolCall: tc,
|
|
4210
|
-
bytesLoaded,
|
|
4211
|
-
totalBytes: after
|
|
5141
|
+
if (toolCallsLen === 0) {
|
|
5142
|
+
bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
|
|
5143
|
+
finishedNormally = true;
|
|
5144
|
+
return {
|
|
5145
|
+
planText: accumulatedText,
|
|
5146
|
+
budgetExhausted,
|
|
5147
|
+
timedOut: false,
|
|
5148
|
+
finishedNormally,
|
|
5149
|
+
iterations,
|
|
5150
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
5151
|
+
};
|
|
5152
|
+
}
|
|
5153
|
+
applyStepToTurns(turns, stepResult);
|
|
5154
|
+
if (budgetExhausted) {
|
|
5155
|
+
turns.push({
|
|
5156
|
+
role: "user",
|
|
5157
|
+
text: "Budget is exhausted. Finalise the plan with the information you already have. Do not call any more tools. Output the complete plan markdown now."
|
|
4212
5158
|
});
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
5159
|
+
}
|
|
5160
|
+
bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: finishReason });
|
|
5161
|
+
}
|
|
5162
|
+
}
|
|
5163
|
+
async function runPlanner(input5) {
|
|
5164
|
+
const bus = input5.events ?? new PlannerEventBus();
|
|
5165
|
+
const runId = input5.runId ?? newRunId();
|
|
5166
|
+
const cacheEnabled = input5.cacheEnabled ?? true;
|
|
5167
|
+
bus.emit({
|
|
5168
|
+
kind: "started",
|
|
5169
|
+
runId,
|
|
5170
|
+
provider: input5.provider,
|
|
5171
|
+
model: input5.modelId,
|
|
5172
|
+
cacheEnabled
|
|
5173
|
+
});
|
|
5174
|
+
const merged = { ...input5, events: bus, runId };
|
|
5175
|
+
const systemPrompt = input5.systemPrompt;
|
|
5176
|
+
let scoutSummary;
|
|
5177
|
+
let scoutStats;
|
|
5178
|
+
let scoutedSection;
|
|
5179
|
+
const scoutOn = input5.stages?.scout?.enabled !== false;
|
|
5180
|
+
const scoutModel = input5.stages?.scout?.model;
|
|
5181
|
+
const maxScoutFiles = input5.stages?.scout?.maxFiles ?? 12;
|
|
5182
|
+
const scoutMaxOut = input5.stages?.scout?.maxOutputTokens ?? 2048;
|
|
5183
|
+
const scoutModelIdStr = input5.stages?.scout?.modelId ?? "";
|
|
5184
|
+
if (scoutOn && scoutModel) {
|
|
5185
|
+
if (!input5.scoutSystemPrompt?.trim()) {
|
|
5186
|
+
throw new Error(
|
|
5187
|
+
"planner: scout enabled but `scoutSystemPrompt` was not provided. Compose it via composeScoutSystemPrompt() or set stages.scout.enabled = false."
|
|
5188
|
+
);
|
|
5189
|
+
}
|
|
5190
|
+
const t0 = Date.now();
|
|
5191
|
+
bus.emit({ kind: "stage_started", runId, stage: "scout" });
|
|
5192
|
+
try {
|
|
5193
|
+
const scoutRes = await runScout({
|
|
5194
|
+
model: scoutModel,
|
|
5195
|
+
systemPrompt: input5.scoutSystemPrompt,
|
|
5196
|
+
userPrompt: input5.userPrompt,
|
|
5197
|
+
budget: input5.budget,
|
|
5198
|
+
abort: input5.abort,
|
|
5199
|
+
maxTokens: scoutMaxOut
|
|
5200
|
+
});
|
|
5201
|
+
const durationMs = Date.now() - t0;
|
|
5202
|
+
if (scoutRes) {
|
|
5203
|
+
const files = scoutRes.output.selectedFiles.slice(0, maxScoutFiles);
|
|
5204
|
+
const tokensUsed = scoutRes.usage.inputTokens + scoutRes.usage.outputTokens;
|
|
5205
|
+
scoutSummary = {
|
|
5206
|
+
selected: files,
|
|
5207
|
+
reasoning: scoutRes.output.reasoning,
|
|
5208
|
+
durationMs,
|
|
5209
|
+
tokensUsed
|
|
5210
|
+
};
|
|
5211
|
+
scoutStats = scoutStageStats(scoutRes.usage, durationMs);
|
|
5212
|
+
bus.emit({
|
|
5213
|
+
kind: "scout_result",
|
|
5214
|
+
runId,
|
|
5215
|
+
selected: files,
|
|
5216
|
+
reasoning: scoutRes.output.reasoning
|
|
5217
|
+
});
|
|
5218
|
+
scoutedSection = buildScoutedContextSection(
|
|
5219
|
+
input5.root,
|
|
5220
|
+
input5.budget,
|
|
5221
|
+
files,
|
|
5222
|
+
bus,
|
|
5223
|
+
runId,
|
|
5224
|
+
scoutRes.output.readRanges
|
|
5225
|
+
);
|
|
5226
|
+
bus.emit({
|
|
5227
|
+
kind: "stage_complete",
|
|
5228
|
+
runId,
|
|
5229
|
+
stage: "scout",
|
|
5230
|
+
success: true,
|
|
5231
|
+
durationMs,
|
|
5232
|
+
tokensUsed
|
|
5233
|
+
});
|
|
5234
|
+
} else {
|
|
5235
|
+
bus.emit({
|
|
5236
|
+
kind: "stage_complete",
|
|
5237
|
+
runId,
|
|
5238
|
+
stage: "scout",
|
|
5239
|
+
success: false,
|
|
5240
|
+
durationMs
|
|
5241
|
+
});
|
|
5242
|
+
}
|
|
5243
|
+
} catch (err2) {
|
|
5244
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
5245
|
+
if (APICallError2.isInstance(err2)) {
|
|
5246
|
+
const status = err2.statusCode ?? 0;
|
|
5247
|
+
if (status === 401 || status === 403) {
|
|
5248
|
+
throw err2;
|
|
5249
|
+
}
|
|
5250
|
+
if (detectModelNotFound(input5.provider, scoutModelIdStr, status, err2.responseBody ?? "")) {
|
|
5251
|
+
throw err2;
|
|
4242
5252
|
}
|
|
4243
5253
|
}
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
5254
|
+
bus.emit({
|
|
5255
|
+
kind: "stage_complete",
|
|
5256
|
+
runId,
|
|
5257
|
+
stage: "scout",
|
|
5258
|
+
success: false,
|
|
5259
|
+
durationMs: Date.now() - t0,
|
|
5260
|
+
errorMessage: msg.slice(0, 200)
|
|
4251
5261
|
});
|
|
4252
5262
|
}
|
|
4253
|
-
bus.emit({ kind: "turn_complete", runId, turn: iterations, stopReason: response.stopReason });
|
|
4254
5263
|
}
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
const
|
|
4258
|
-
|
|
4259
|
-
|
|
5264
|
+
const draftStarted = Date.now();
|
|
5265
|
+
bus.emit({ kind: "stage_started", runId, stage: "draft" });
|
|
5266
|
+
const draft = await runDraftStage({ ...merged, systemPrompt, scoutedSection });
|
|
5267
|
+
bus.emit({
|
|
5268
|
+
kind: "stage_complete",
|
|
5269
|
+
runId,
|
|
5270
|
+
stage: "draft",
|
|
5271
|
+
success: draft.finishedNormally && !draft.timedOut && !draft.userCancelled,
|
|
5272
|
+
durationMs: Date.now() - draftStarted
|
|
5273
|
+
});
|
|
5274
|
+
const validationOn = input5.validation?.enabled !== false;
|
|
5275
|
+
let issues = [];
|
|
5276
|
+
let validationDurationMs = 0;
|
|
5277
|
+
if (validationOn && draft.planText.trim()) {
|
|
5278
|
+
const v0 = Date.now();
|
|
5279
|
+
bus.emit({ kind: "stage_started", runId, stage: "validation" });
|
|
5280
|
+
issues = validatePlan({ root: input5.root, planText: draft.planText });
|
|
5281
|
+
for (const iss of issues.slice(0, 100)) {
|
|
5282
|
+
bus.emit({
|
|
5283
|
+
kind: "validation_issue",
|
|
5284
|
+
runId,
|
|
5285
|
+
severity: iss.severity,
|
|
5286
|
+
issueKind: iss.kind,
|
|
5287
|
+
path: iss.path,
|
|
5288
|
+
detail: iss.detail,
|
|
5289
|
+
excerpt: iss.excerpt
|
|
5290
|
+
});
|
|
5291
|
+
}
|
|
5292
|
+
validationDurationMs = Date.now() - v0;
|
|
5293
|
+
bus.emit({
|
|
5294
|
+
kind: "stage_complete",
|
|
5295
|
+
runId,
|
|
5296
|
+
stage: "validation",
|
|
5297
|
+
success: true,
|
|
5298
|
+
durationMs: validationDurationMs
|
|
5299
|
+
});
|
|
4260
5300
|
}
|
|
4261
|
-
return
|
|
5301
|
+
return {
|
|
5302
|
+
...draft,
|
|
5303
|
+
stats: draft.stats,
|
|
5304
|
+
stagesStats: { scout: scoutStats, draft: draft.stats },
|
|
5305
|
+
scout: scoutSummary,
|
|
5306
|
+
validation: validationOn ? { issues, durationMs: validationDurationMs } : { issues: [], durationMs: 0 }
|
|
5307
|
+
};
|
|
4262
5308
|
}
|
|
4263
5309
|
function composePlannerError(providerName, response, retriedRateLimit, retrySkippedReason) {
|
|
4264
5310
|
if (response.errorKind === "rate_limit") {
|
|
@@ -4297,45 +5343,39 @@ function kindTitle(kind) {
|
|
|
4297
5343
|
return "Model round limit";
|
|
4298
5344
|
case "wall_clock":
|
|
4299
5345
|
return "Wall-clock time limit";
|
|
4300
|
-
case "cost_cap":
|
|
4301
|
-
return "Estimated cost cap";
|
|
4302
5346
|
case "file_or_context_reads":
|
|
4303
5347
|
return "File read or context size limit";
|
|
4304
5348
|
}
|
|
4305
5349
|
}
|
|
4306
|
-
function
|
|
5350
|
+
function tokenSummary(ctx) {
|
|
4307
5351
|
const snap = ctx.budgetSnapshot;
|
|
4308
|
-
|
|
5352
|
+
return `Tokens so far: ${snap.usage.inputTokens} in / ${snap.usage.outputTokens} out for this session.`;
|
|
5353
|
+
}
|
|
5354
|
+
function kindDetail(ctx) {
|
|
4309
5355
|
switch (ctx.kind) {
|
|
4310
5356
|
case "max_output_tokens":
|
|
4311
5357
|
return [
|
|
4312
5358
|
`The model hit the per-request output cap (${ctx.maxOutputTokens} completion tokens). Long plans can stop mid-markdown even though the session is otherwise healthy.`,
|
|
4313
5359
|
"You can continue: squad-kit will ask the model to append the rest of the plan in a follow-up request (extra tokens apply).",
|
|
4314
|
-
|
|
5360
|
+
tokenSummary(ctx)
|
|
4315
5361
|
];
|
|
4316
5362
|
case "max_iterations":
|
|
4317
5363
|
return [
|
|
4318
5364
|
`The planner reached its round cap (${ctx.maxIterations} model turns) without a clean stop.`,
|
|
4319
5365
|
"Continuing raises the round cap and file/time budgets for this run by another full slice from your config.",
|
|
4320
|
-
|
|
5366
|
+
tokenSummary(ctx)
|
|
4321
5367
|
];
|
|
4322
5368
|
case "wall_clock":
|
|
4323
5369
|
return [
|
|
4324
5370
|
"The configured wall-clock budget for this planning session elapsed before the model finished.",
|
|
4325
5371
|
"Continuing adds another slice of time (and read/output/round limits) from your planner budget.",
|
|
4326
|
-
|
|
4327
|
-
];
|
|
4328
|
-
case "cost_cap":
|
|
4329
|
-
return [
|
|
4330
|
-
"Spending crossed the optional `planner.budget.maxCostUsd` ceiling for this session.",
|
|
4331
|
-
"Continuing raises that ceiling for this run by another slice from config (if a cap is set).",
|
|
4332
|
-
cost
|
|
5372
|
+
tokenSummary(ctx)
|
|
4333
5373
|
];
|
|
4334
5374
|
case "file_or_context_reads":
|
|
4335
5375
|
return [
|
|
4336
5376
|
"A `read_file` call could not run because the file-read count or total read-bytes budget was exhausted.",
|
|
4337
5377
|
"Continuing extends read, context, time, round, and output limits for this session so the model can read again or finish without reads.",
|
|
4338
|
-
|
|
5378
|
+
tokenSummary(ctx)
|
|
4339
5379
|
];
|
|
4340
5380
|
}
|
|
4341
5381
|
}
|
|
@@ -4359,14 +5399,17 @@ function printPlannerLimitNextSteps() {
|
|
|
4359
5399
|
}
|
|
4360
5400
|
|
|
4361
5401
|
// src/planner/providers/index.ts
|
|
4362
|
-
|
|
4363
|
-
|
|
5402
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
5403
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
5404
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
5405
|
+
function resolveModel(provider, modelId, apiKey) {
|
|
5406
|
+
switch (provider) {
|
|
4364
5407
|
case "anthropic":
|
|
4365
|
-
return
|
|
5408
|
+
return { provider, model: createAnthropic({ apiKey })(modelId), modelId };
|
|
4366
5409
|
case "openai":
|
|
4367
|
-
return
|
|
5410
|
+
return { provider, model: createOpenAI({ apiKey })(modelId), modelId };
|
|
4368
5411
|
case "google":
|
|
4369
|
-
return
|
|
5412
|
+
return { provider, model: createGoogleGenerativeAI({ apiKey })(modelId), modelId };
|
|
4370
5413
|
}
|
|
4371
5414
|
}
|
|
4372
5415
|
|
|
@@ -4377,7 +5420,6 @@ var Budget = class {
|
|
|
4377
5420
|
this.maxFileReadsAllowed = cfg.maxFileReads;
|
|
4378
5421
|
this.maxContextBytesAllowed = cfg.maxContextBytes;
|
|
4379
5422
|
this.maxWallSeconds = cfg.maxDurationSeconds;
|
|
4380
|
-
this.maxCostUsdCeiling = cfg.maxCostUsd;
|
|
4381
5423
|
}
|
|
4382
5424
|
cfg;
|
|
4383
5425
|
reads = 0;
|
|
@@ -4387,15 +5429,11 @@ var Budget = class {
|
|
|
4387
5429
|
maxFileReadsAllowed;
|
|
4388
5430
|
maxContextBytesAllowed;
|
|
4389
5431
|
maxWallSeconds;
|
|
4390
|
-
maxCostUsdCeiling;
|
|
4391
5432
|
/** Add another slice of the original config limits (used when the user chooses to continue a planner session). */
|
|
4392
5433
|
extendSession() {
|
|
4393
5434
|
this.maxFileReadsAllowed += this.cfg.maxFileReads;
|
|
4394
5435
|
this.maxContextBytesAllowed += this.cfg.maxContextBytes;
|
|
4395
5436
|
this.maxWallSeconds += this.cfg.maxDurationSeconds;
|
|
4396
|
-
if (this.cfg.maxCostUsd !== void 0) {
|
|
4397
|
-
this.maxCostUsdCeiling = (this.maxCostUsdCeiling ?? 0) + this.cfg.maxCostUsd;
|
|
4398
|
-
}
|
|
4399
5437
|
}
|
|
4400
5438
|
canRead(nextFileBytes) {
|
|
4401
5439
|
if (this.reads >= this.maxFileReadsAllowed) {
|
|
@@ -4406,6 +5444,13 @@ var Budget = class {
|
|
|
4406
5444
|
}
|
|
4407
5445
|
return { ok: true };
|
|
4408
5446
|
}
|
|
5447
|
+
/** Whether another tool invocation is allowed under the read-count budget (bytes checked per tool). */
|
|
5448
|
+
canExecuteTool() {
|
|
5449
|
+
if (this.reads >= this.maxFileReadsAllowed) {
|
|
5450
|
+
return { ok: false, reason: `max file reads (${this.maxFileReadsAllowed}) reached` };
|
|
5451
|
+
}
|
|
5452
|
+
return { ok: true };
|
|
5453
|
+
}
|
|
4409
5454
|
recordRead(bytes) {
|
|
4410
5455
|
this.reads += 1;
|
|
4411
5456
|
this.bytes += bytes;
|
|
@@ -4413,17 +5458,12 @@ var Budget = class {
|
|
|
4413
5458
|
recordUsage(u) {
|
|
4414
5459
|
this.totalUsage.inputTokens += u.inputTokens;
|
|
4415
5460
|
this.totalUsage.outputTokens += u.outputTokens;
|
|
4416
|
-
if (u.costUsd !== void 0) this.totalUsage.costUsd = (this.totalUsage.costUsd ?? 0) + u.costUsd;
|
|
4417
5461
|
this.totalUsage.cacheCreationTokens = (this.totalUsage.cacheCreationTokens ?? 0) + (u.cacheCreationTokens ?? 0);
|
|
4418
5462
|
this.totalUsage.cacheReadTokens = (this.totalUsage.cacheReadTokens ?? 0) + (u.cacheReadTokens ?? 0);
|
|
4419
5463
|
}
|
|
4420
5464
|
timedOut() {
|
|
4421
5465
|
return (Date.now() - this.startedAt) / 1e3 > this.maxWallSeconds;
|
|
4422
5466
|
}
|
|
4423
|
-
overCost() {
|
|
4424
|
-
if (this.maxCostUsdCeiling === void 0) return false;
|
|
4425
|
-
return (this.totalUsage.costUsd ?? 0) > this.maxCostUsdCeiling;
|
|
4426
|
-
}
|
|
4427
5467
|
snapshot() {
|
|
4428
5468
|
return {
|
|
4429
5469
|
reads: this.reads,
|
|
@@ -4434,103 +5474,9 @@ var Budget = class {
|
|
|
4434
5474
|
}
|
|
4435
5475
|
};
|
|
4436
5476
|
|
|
4437
|
-
// src/core/repo-map.ts
|
|
4438
|
-
import fs23 from "fs";
|
|
4439
|
-
import path16 from "path";
|
|
4440
|
-
import ignore from "ignore";
|
|
4441
|
-
var DEFAULT_IGNORES = [
|
|
4442
|
-
"node_modules",
|
|
4443
|
-
".git",
|
|
4444
|
-
"dist",
|
|
4445
|
-
"build",
|
|
4446
|
-
"coverage",
|
|
4447
|
-
".next",
|
|
4448
|
-
".turbo",
|
|
4449
|
-
".cache",
|
|
4450
|
-
".squad/plans"
|
|
4451
|
-
// don't include existing plans as context; planner will see them separately
|
|
4452
|
-
];
|
|
4453
|
-
function buildRepoMap(root, opts = {}) {
|
|
4454
|
-
const maxEntries = opts.maxEntries ?? 5e3;
|
|
4455
|
-
const ig = buildIgnore(root);
|
|
4456
|
-
const entries = [];
|
|
4457
|
-
walk2(root, root, ig, entries, maxEntries, !!opts.includeHidden);
|
|
4458
|
-
entries.sort();
|
|
4459
|
-
return entries.join("\n") + "\n";
|
|
4460
|
-
}
|
|
4461
|
-
function buildIgnore(root) {
|
|
4462
|
-
const ig = ignore().add(DEFAULT_IGNORES);
|
|
4463
|
-
const gitignore = path16.join(root, ".gitignore");
|
|
4464
|
-
if (fs23.existsSync(gitignore)) {
|
|
4465
|
-
try {
|
|
4466
|
-
ig.add(fs23.readFileSync(gitignore, "utf8"));
|
|
4467
|
-
} catch {
|
|
4468
|
-
}
|
|
4469
|
-
}
|
|
4470
|
-
return ig;
|
|
4471
|
-
}
|
|
4472
|
-
function walk2(root, dir, ig, out, max, includeHidden) {
|
|
4473
|
-
if (out.length >= max) return;
|
|
4474
|
-
let entries;
|
|
4475
|
-
try {
|
|
4476
|
-
entries = fs23.readdirSync(dir, { withFileTypes: true });
|
|
4477
|
-
} catch {
|
|
4478
|
-
return;
|
|
4479
|
-
}
|
|
4480
|
-
for (const entry of entries) {
|
|
4481
|
-
if (out.length >= max) return;
|
|
4482
|
-
if (!includeHidden && entry.name.startsWith(".") && entry.name !== ".squad") continue;
|
|
4483
|
-
const abs = path16.join(dir, entry.name);
|
|
4484
|
-
const rel = path16.relative(root, abs);
|
|
4485
|
-
if (!rel || ig.ignores(rel) || entry.isDirectory() && ig.ignores(rel + "/")) continue;
|
|
4486
|
-
if (entry.isDirectory()) {
|
|
4487
|
-
walk2(root, abs, ig, out, max, includeHidden);
|
|
4488
|
-
} else if (entry.isFile()) {
|
|
4489
|
-
out.push(rel);
|
|
4490
|
-
}
|
|
4491
|
-
}
|
|
4492
|
-
}
|
|
4493
|
-
|
|
4494
|
-
// src/planner/system-prompt.ts
|
|
4495
|
-
function composeSystemPrompt(args) {
|
|
4496
|
-
const base = readBundledPrompt("generate-plan.md").replace(/\{\{projectRoots\}\}/g, args.projectRoots.join(", ")).replace(/\{\{primaryLanguage\}\}/g, args.primaryLanguage).replace(/\{\{trackerType\}\}/g, args.trackerType).replace(
|
|
4497
|
-
/\{\{intakeContent\}\}/g,
|
|
4498
|
-
"_(The intake story is provided in the user message below. Do not ask for it.)_"
|
|
4499
|
-
);
|
|
4500
|
-
const apiPreamble = [
|
|
4501
|
-
"",
|
|
4502
|
-
"---",
|
|
4503
|
-
"",
|
|
4504
|
-
"## Direct-API mode notes",
|
|
4505
|
-
"",
|
|
4506
|
-
"You are being invoked by the squad-kit CLI, not by a human editor. The intake is already inlined below.",
|
|
4507
|
-
"The repository tree is provided. To see the contents of any file, **call the `read_file` tool** with a repo-relative path.",
|
|
4508
|
-
"You have a bounded context budget (file reads, total bytes, wall-clock time). Read files you genuinely need.",
|
|
4509
|
-
"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.",
|
|
4510
|
-
"",
|
|
4511
|
-
"## Repository tree",
|
|
4512
|
-
"",
|
|
4513
|
-
"```",
|
|
4514
|
-
args.repoMap.trimEnd(),
|
|
4515
|
-
"```",
|
|
4516
|
-
""
|
|
4517
|
-
].join("\n");
|
|
4518
|
-
return base + apiPreamble;
|
|
4519
|
-
}
|
|
4520
|
-
function composeUserPrompt(args) {
|
|
4521
|
-
return [
|
|
4522
|
-
"Produce the implementation plan for the following intake.",
|
|
4523
|
-
"",
|
|
4524
|
-
"---",
|
|
4525
|
-
"",
|
|
4526
|
-
args.intakeContent.trimEnd(),
|
|
4527
|
-
""
|
|
4528
|
-
].join("\n");
|
|
4529
|
-
}
|
|
4530
|
-
|
|
4531
5477
|
// src/planner/writer.ts
|
|
4532
|
-
import
|
|
4533
|
-
import
|
|
5478
|
+
import fs28 from "fs";
|
|
5479
|
+
import path20 from "path";
|
|
4534
5480
|
function planFileSlugParts(config2, story) {
|
|
4535
5481
|
const baseSlug = slugify(story.id);
|
|
4536
5482
|
const trackerId = config2.naming.includeTrackerId && config2.tracker.type !== "none" ? trackerIdForFilename(config2.tracker.type, story.id) : "";
|
|
@@ -4539,8 +5485,8 @@ function planFileSlugParts(config2, story) {
|
|
|
4539
5485
|
}
|
|
4540
5486
|
function writePlanFile(input5) {
|
|
4541
5487
|
const { paths, config: config2, story } = input5;
|
|
4542
|
-
const featurePlanDir =
|
|
4543
|
-
|
|
5488
|
+
const featurePlanDir = path20.join(paths.plansDir, story.feature);
|
|
5489
|
+
fs28.mkdirSync(featurePlanDir, { recursive: true });
|
|
4544
5490
|
if (input5.partial) {
|
|
4545
5491
|
return writePartialPlanFile(input5, featurePlanDir, paths, config2, story);
|
|
4546
5492
|
}
|
|
@@ -4555,14 +5501,14 @@ function writePlanFile(input5) {
|
|
|
4555
5501
|
} else {
|
|
4556
5502
|
nn = scanPlans(paths.plansDir).nextGlobal;
|
|
4557
5503
|
const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
|
|
4558
|
-
targetPath =
|
|
5504
|
+
targetPath = path20.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.md`);
|
|
4559
5505
|
}
|
|
4560
5506
|
const body = `${input5.metadataHeader.trimEnd()}
|
|
4561
5507
|
|
|
4562
5508
|
${input5.planBodyMarkdown.trimStart()}
|
|
4563
5509
|
`;
|
|
4564
|
-
|
|
4565
|
-
upsertOverviewRow(featurePlanDir, story.feature, nn,
|
|
5510
|
+
fs28.writeFileSync(targetPath, body, "utf8");
|
|
5511
|
+
upsertOverviewRow(featurePlanDir, story.feature, nn, path20.basename(targetPath), story, false);
|
|
4566
5512
|
return { planFile: targetPath, sequenceNumber: nn, overwrote };
|
|
4567
5513
|
}
|
|
4568
5514
|
function writePartialPlanFile(input5, featurePlanDir, paths, config2, story) {
|
|
@@ -4577,42 +5523,42 @@ function writePartialPlanFile(input5, featurePlanDir, paths, config2, story) {
|
|
|
4577
5523
|
} else {
|
|
4578
5524
|
nn = scanPlans(paths.plansDir).nextGlobal;
|
|
4579
5525
|
const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
|
|
4580
|
-
targetPath =
|
|
5526
|
+
targetPath = path20.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.partial.md`);
|
|
4581
5527
|
}
|
|
4582
5528
|
const body = `${input5.metadataHeader.trimEnd()}
|
|
4583
5529
|
|
|
4584
5530
|
${input5.planBodyMarkdown.trimStart()}
|
|
4585
5531
|
`;
|
|
4586
|
-
|
|
4587
|
-
upsertOverviewRow(featurePlanDir, story.feature, nn,
|
|
5532
|
+
fs28.writeFileSync(targetPath, body, "utf8");
|
|
5533
|
+
upsertOverviewRow(featurePlanDir, story.feature, nn, path20.basename(targetPath), story, true);
|
|
4588
5534
|
return { planFile: targetPath, sequenceNumber: nn, overwrote };
|
|
4589
5535
|
}
|
|
4590
5536
|
function findExistingPlan(featureDir, storyId) {
|
|
4591
|
-
if (!
|
|
5537
|
+
if (!fs28.existsSync(featureDir)) return void 0;
|
|
4592
5538
|
const pattern = /^(\d{2,})-story-.+\.md$/;
|
|
4593
|
-
for (const entry of
|
|
5539
|
+
for (const entry of fs28.readdirSync(featureDir)) {
|
|
4594
5540
|
if (entry.endsWith(".partial.md")) continue;
|
|
4595
5541
|
const m = entry.match(pattern);
|
|
4596
5542
|
if (!m) continue;
|
|
4597
|
-
if (entry.includes(storyId)) return { absPath:
|
|
5543
|
+
if (entry.includes(storyId)) return { absPath: path20.join(featureDir, entry), nn: parseInt(m[1], 10) };
|
|
4598
5544
|
}
|
|
4599
5545
|
return void 0;
|
|
4600
5546
|
}
|
|
4601
5547
|
function findExistingPartialPlan(featureDir, storyId) {
|
|
4602
|
-
if (!
|
|
5548
|
+
if (!fs28.existsSync(featureDir)) return void 0;
|
|
4603
5549
|
const pattern = /^(\d{2,})-story-.+\.partial\.md$/;
|
|
4604
|
-
for (const entry of
|
|
5550
|
+
for (const entry of fs28.readdirSync(featureDir)) {
|
|
4605
5551
|
const m = entry.match(pattern);
|
|
4606
5552
|
if (!m) continue;
|
|
4607
|
-
if (entry.includes(storyId)) return { absPath:
|
|
5553
|
+
if (entry.includes(storyId)) return { absPath: path20.join(featureDir, entry), nn: parseInt(m[1], 10) };
|
|
4608
5554
|
}
|
|
4609
5555
|
return void 0;
|
|
4610
5556
|
}
|
|
4611
5557
|
function upsertOverviewRow(featureDir, feature, nn, filename, story, isPartial) {
|
|
4612
|
-
const overviewPath =
|
|
5558
|
+
const overviewPath = path20.join(featureDir, "00-overview.md");
|
|
4613
5559
|
const titleCell = isPartial ? `${story.titleHint ?? story.id} (partial)` : story.titleHint ?? story.id;
|
|
4614
5560
|
const row = `| ${formatSequence(nn)} | \`${filename}\` | ${titleCell} | ${story.id} | \u2014 |`;
|
|
4615
|
-
if (!
|
|
5561
|
+
if (!fs28.existsSync(overviewPath)) {
|
|
4616
5562
|
const content = `# ${feature} \u2014 plan overview
|
|
4617
5563
|
|
|
4618
5564
|
Entry point for the **${feature}** feature. Stories execute in order by their \`NN\` prefix.
|
|
@@ -4623,10 +5569,10 @@ Entry point for the **${feature}** feature. Stories execute in order by their \`
|
|
|
4623
5569
|
|----|------|-------|------------|------------|
|
|
4624
5570
|
${row}
|
|
4625
5571
|
`;
|
|
4626
|
-
|
|
5572
|
+
fs28.writeFileSync(overviewPath, content, "utf8");
|
|
4627
5573
|
return;
|
|
4628
5574
|
}
|
|
4629
|
-
const current =
|
|
5575
|
+
const current = fs28.readFileSync(overviewPath, "utf8");
|
|
4630
5576
|
const lines = current.split("\n");
|
|
4631
5577
|
const existingIdx = lines.findIndex((l) => l.includes(`\`${filename}\``) || l.includes(`| ${formatSequence(nn)} |`));
|
|
4632
5578
|
if (existingIdx >= 0) {
|
|
@@ -4635,7 +5581,7 @@ ${row}
|
|
|
4635
5581
|
const tableEnd = findTableEnd(lines);
|
|
4636
5582
|
lines.splice(tableEnd, 0, row);
|
|
4637
5583
|
}
|
|
4638
|
-
|
|
5584
|
+
fs28.writeFileSync(overviewPath, lines.join("\n"), "utf8");
|
|
4639
5585
|
}
|
|
4640
5586
|
function findTableEnd(lines) {
|
|
4641
5587
|
let inTable = false;
|
|
@@ -4654,9 +5600,17 @@ function buildMetadataHeader(args) {
|
|
|
4654
5600
|
`generated by ${args.provider}/${args.model}`,
|
|
4655
5601
|
`at ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
4656
5602
|
`${args.reads} reads / ${(args.bytes / 1024).toFixed(1)} KB context`,
|
|
4657
|
-
`${args.inputTokens} in / ${args.outputTokens} out tokens
|
|
4658
|
-
args.costUsd !== void 0 ? `~$${args.costUsd.toFixed(2)}` : null
|
|
5603
|
+
`${args.inputTokens} in / ${args.outputTokens} out tokens`
|
|
4659
5604
|
].filter(Boolean);
|
|
5605
|
+
if (args.scoutEnabled !== void 0) {
|
|
5606
|
+
parts.push(`scout=${args.scoutEnabled ? "on" : "off"}`);
|
|
5607
|
+
}
|
|
5608
|
+
if (args.validationEnabled !== void 0) {
|
|
5609
|
+
parts.push(`validation=${args.validationEnabled ? "on" : "off"}`);
|
|
5610
|
+
}
|
|
5611
|
+
if (args.validationIssueCount !== void 0) {
|
|
5612
|
+
parts.push(`issues=${args.validationIssueCount}`);
|
|
5613
|
+
}
|
|
4660
5614
|
const line1 = `<!-- squad-kit: ${parts.join(", ")} -->`;
|
|
4661
5615
|
if (args.planStatus === "partial") {
|
|
4662
5616
|
return `${line1}
|
|
@@ -4680,16 +5634,16 @@ function buildCopyPlanPromptMarkdown(config2, intakeContent) {
|
|
|
4680
5634
|
}
|
|
4681
5635
|
|
|
4682
5636
|
// src/core/last-run.ts
|
|
4683
|
-
import
|
|
4684
|
-
import
|
|
5637
|
+
import fs29 from "fs/promises";
|
|
5638
|
+
import path21 from "path";
|
|
4685
5639
|
async function writeLastRun(paths, rec) {
|
|
4686
|
-
const file =
|
|
4687
|
-
await
|
|
5640
|
+
const file = path21.join(paths.squadDir, ".last-run.json");
|
|
5641
|
+
await fs29.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
|
|
4688
5642
|
}
|
|
4689
5643
|
async function readLastRun(paths) {
|
|
4690
|
-
const file =
|
|
5644
|
+
const file = path21.join(paths.squadDir, ".last-run.json");
|
|
4691
5645
|
try {
|
|
4692
|
-
const raw = await
|
|
5646
|
+
const raw = await fs29.readFile(file, "utf8");
|
|
4693
5647
|
const parsed = JSON.parse(raw);
|
|
4694
5648
|
if (parsed.version !== 1) return void 0;
|
|
4695
5649
|
return parsed;
|
|
@@ -4718,6 +5672,59 @@ function formatPlannerCacheLine(opts) {
|
|
|
4718
5672
|
return "cache miss 0% \u2014 run `squad doctor` to diagnose";
|
|
4719
5673
|
}
|
|
4720
5674
|
|
|
5675
|
+
// src/core/plans-index.ts
|
|
5676
|
+
import fs30 from "fs";
|
|
5677
|
+
import path22 from "path";
|
|
5678
|
+
var MAX_ENTRIES = 30;
|
|
5679
|
+
function firstMarkdownH1(src) {
|
|
5680
|
+
const lines = src.split(/\r?\n/);
|
|
5681
|
+
for (const line2 of lines) {
|
|
5682
|
+
const m = /^\s*#\s+(.+?)\s*$/.exec(line2);
|
|
5683
|
+
if (m) return m[1].trim();
|
|
5684
|
+
}
|
|
5685
|
+
return void 0;
|
|
5686
|
+
}
|
|
5687
|
+
function collectPlanMarkdownFiles(plansDir) {
|
|
5688
|
+
const out = [];
|
|
5689
|
+
const walk2 = (dir) => {
|
|
5690
|
+
let ents;
|
|
5691
|
+
try {
|
|
5692
|
+
ents = fs30.readdirSync(dir, { withFileTypes: true });
|
|
5693
|
+
} catch {
|
|
5694
|
+
return;
|
|
5695
|
+
}
|
|
5696
|
+
for (const ent of ents) {
|
|
5697
|
+
const abs = path22.join(dir, ent.name);
|
|
5698
|
+
if (ent.isDirectory()) {
|
|
5699
|
+
walk2(abs);
|
|
5700
|
+
} else if (ent.isFile() && ent.name.endsWith(".md")) {
|
|
5701
|
+
if (ent.name === "00-index.md" || ent.name === "00-overview.md") continue;
|
|
5702
|
+
out.push(abs);
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5705
|
+
};
|
|
5706
|
+
if (fs30.existsSync(plansDir)) walk2(plansDir);
|
|
5707
|
+
out.sort((a, b) => a.localeCompare(b));
|
|
5708
|
+
return out.slice(0, MAX_ENTRIES);
|
|
5709
|
+
}
|
|
5710
|
+
function buildPlansIndex(paths) {
|
|
5711
|
+
const files = collectPlanMarkdownFiles(paths.plansDir);
|
|
5712
|
+
if (files.length === 0) return "(no prior plans yet)";
|
|
5713
|
+
const bullets = [];
|
|
5714
|
+
for (const abs of files) {
|
|
5715
|
+
const rel = path22.relative(paths.root, abs).split(path22.sep).join("/");
|
|
5716
|
+
let title = "";
|
|
5717
|
+
try {
|
|
5718
|
+
const raw = fs30.readFileSync(abs, "utf8");
|
|
5719
|
+
title = firstMarkdownH1(raw) ?? path22.basename(abs);
|
|
5720
|
+
} catch {
|
|
5721
|
+
title = path22.basename(abs);
|
|
5722
|
+
}
|
|
5723
|
+
bullets.push(`- ${rel} \u2014 "${title}"`);
|
|
5724
|
+
}
|
|
5725
|
+
return bullets.join("\n");
|
|
5726
|
+
}
|
|
5727
|
+
|
|
4721
5728
|
// src/commands/new-plan.ts
|
|
4722
5729
|
function decideMode(opts, config2) {
|
|
4723
5730
|
if (opts.api && opts.copy) {
|
|
@@ -4749,8 +5756,8 @@ async function runNewPlan(intakePath, opts) {
|
|
|
4749
5756
|
}
|
|
4750
5757
|
}
|
|
4751
5758
|
function resolveFromPath(intakePath, stories, root) {
|
|
4752
|
-
const resolved =
|
|
4753
|
-
if (!
|
|
5759
|
+
const resolved = path23.resolve(intakePath.trim());
|
|
5760
|
+
if (!fs31.existsSync(resolved)) {
|
|
4754
5761
|
throw new Error(
|
|
4755
5762
|
`Intake not found: ${resolved}. Run \`squad list\` to see intakes, or \`squad new-story\` to create one first.`
|
|
4756
5763
|
);
|
|
@@ -4762,7 +5769,7 @@ function resolveFromPath(intakePath, stories, root) {
|
|
|
4762
5769
|
);
|
|
4763
5770
|
}
|
|
4764
5771
|
function relativeStoriesDir(root) {
|
|
4765
|
-
return
|
|
5772
|
+
return path23.relative(root, path23.join(root, ".squad", "stories"));
|
|
4766
5773
|
}
|
|
4767
5774
|
async function pickStory(stories, opts) {
|
|
4768
5775
|
const candidates = opts.all ? stories : stories.filter((s) => !s.planFile);
|
|
@@ -4793,7 +5800,7 @@ async function pickStory(stories, opts) {
|
|
|
4793
5800
|
value: s.intakePath
|
|
4794
5801
|
}))
|
|
4795
5802
|
});
|
|
4796
|
-
return candidates.find((s) =>
|
|
5803
|
+
return candidates.find((s) => path23.resolve(s.intakePath) === path23.resolve(pick));
|
|
4797
5804
|
}
|
|
4798
5805
|
function formatLabel(s) {
|
|
4799
5806
|
const head = `${s.feature} / ${s.id}`;
|
|
@@ -4839,13 +5846,13 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4839
5846
|
`Missing ${providerEnvVar(planner.provider)}. Run \`squad config set planner\` to save a key to secrets, or export the env var, or run \`squad new-plan --copy\` without the API.`
|
|
4840
5847
|
);
|
|
4841
5848
|
}
|
|
4842
|
-
const
|
|
4843
|
-
const
|
|
5849
|
+
const modelId = modelFor(planner.provider, "plan", planner.modelOverride);
|
|
5850
|
+
const { model } = resolveModel(planner.provider, modelId, apiKey);
|
|
4844
5851
|
const budget = new Budget(planner.budget);
|
|
4845
5852
|
banner();
|
|
4846
5853
|
step(`planning ${story.feature} / ${story.id}`);
|
|
4847
5854
|
kv("provider", planner.provider);
|
|
4848
|
-
kv("model",
|
|
5855
|
+
kv("model", modelId);
|
|
4849
5856
|
kv(
|
|
4850
5857
|
"budget",
|
|
4851
5858
|
`${planner.budget.maxFileReads} reads \xB7 ${(planner.budget.maxContextBytes / 1024).toFixed(0)} KB \xB7 ${planner.budget.maxDurationSeconds}s`
|
|
@@ -4856,7 +5863,7 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4856
5863
|
printPlannerApiCostNotice();
|
|
4857
5864
|
}
|
|
4858
5865
|
const mapSpinner = spinner("building repo map\u2026");
|
|
4859
|
-
const repoMap = buildRepoMap(paths.root);
|
|
5866
|
+
const repoMap = buildRepoMap(paths.root, { format: "tree" });
|
|
4860
5867
|
mapSpinner.succeed(
|
|
4861
5868
|
`repo map ready (${repoMap.split("\n").length - 1} paths \xB7 ${(repoMap.length / 1024).toFixed(1)} KB)`
|
|
4862
5869
|
);
|
|
@@ -4868,6 +5875,29 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4868
5875
|
repoMap
|
|
4869
5876
|
});
|
|
4870
5877
|
const userPrompt = composeUserPrompt({ intakeContent: readFile(story.intakePath) });
|
|
5878
|
+
const scoutEnabled = !opts.noScout && planner.stages?.scout?.enabled !== false;
|
|
5879
|
+
const validationEnabled = !opts.noValidation && planner.validation?.enabled !== false;
|
|
5880
|
+
const strictValidation = !!opts.strictValidation || planner.validation?.strict === true;
|
|
5881
|
+
const maxScoutFiles = opts.maxScoutFiles ?? planner.stages?.scout?.maxFiles ?? 12;
|
|
5882
|
+
let scoutModelId = "";
|
|
5883
|
+
let scoutModelHandle;
|
|
5884
|
+
let scoutSystemPrompt;
|
|
5885
|
+
if (scoutEnabled) {
|
|
5886
|
+
scoutModelId = modelFor(
|
|
5887
|
+
planner.provider,
|
|
5888
|
+
"scout",
|
|
5889
|
+
planner.modelOverride,
|
|
5890
|
+
opts.scoutModel ?? planner.stages?.scout?.modelOverride
|
|
5891
|
+
);
|
|
5892
|
+
scoutModelHandle = resolveModel(planner.provider, scoutModelId, apiKey).model;
|
|
5893
|
+
scoutSystemPrompt = composeScoutSystemPrompt({
|
|
5894
|
+
projectRoots: config2.project.projectRoots ?? ["."],
|
|
5895
|
+
primaryLanguage: config2.project.primaryLanguage ?? "",
|
|
5896
|
+
trackerType: config2.tracker.type,
|
|
5897
|
+
repoMap,
|
|
5898
|
+
plansIndex: buildPlansIndex(paths)
|
|
5899
|
+
});
|
|
5900
|
+
}
|
|
4871
5901
|
const sessionSpinner = { current: null };
|
|
4872
5902
|
const startedAt = Date.now();
|
|
4873
5903
|
const cacheEnabled = planner.cache?.enabled ?? true;
|
|
@@ -4876,11 +5906,43 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4876
5906
|
const unsubscribe = bus.subscribe((e) => {
|
|
4877
5907
|
switch (e.kind) {
|
|
4878
5908
|
case "tool_call": {
|
|
4879
|
-
const
|
|
5909
|
+
const name = e.toolCall.name;
|
|
5910
|
+
const inp = e.toolCall.input;
|
|
5911
|
+
let label;
|
|
5912
|
+
if (name === "read_file") {
|
|
5913
|
+
label = `read ${String(inp.path ?? "<unknown>")}`;
|
|
5914
|
+
} else if (name === "grep") {
|
|
5915
|
+
label = `grep ${String(inp.pattern ?? "?").slice(0, 48)}`;
|
|
5916
|
+
} else if (name === "list_dir") {
|
|
5917
|
+
label = `list_dir ${String(inp.path ?? "?")}`;
|
|
5918
|
+
} else {
|
|
5919
|
+
label = name;
|
|
5920
|
+
}
|
|
4880
5921
|
sessionSpinner.current?.succeed(
|
|
4881
|
-
|
|
5922
|
+
`${label} (${(e.bytesLoaded / 1024).toFixed(1)} KB \xB7 ${(e.totalBytes / 1024).toFixed(1)} KB / ${(planner.budget.maxContextBytes / 1024).toFixed(0)} KB)`
|
|
4882
5923
|
);
|
|
4883
|
-
sessionSpinner.current = spinner("
|
|
5924
|
+
sessionSpinner.current = spinner("next tool\u2026");
|
|
5925
|
+
break;
|
|
5926
|
+
}
|
|
5927
|
+
case "stage_started": {
|
|
5928
|
+
sessionSpinner.current?.stop();
|
|
5929
|
+
if (e.stage === "scout") sessionSpinner.current = spinner("scouting\u2026");
|
|
5930
|
+
else if (e.stage === "draft") sessionSpinner.current = spinner("drafting\u2026");
|
|
5931
|
+
else if (e.stage === "validation") sessionSpinner.current = spinner("validating\u2026");
|
|
5932
|
+
break;
|
|
5933
|
+
}
|
|
5934
|
+
case "stage_complete": {
|
|
5935
|
+
if (!e.success && e.errorMessage && e.stage === "scout") {
|
|
5936
|
+
sessionSpinner.current?.stop();
|
|
5937
|
+
warning(`scout stage failed: ${e.errorMessage}`);
|
|
5938
|
+
}
|
|
5939
|
+
break;
|
|
5940
|
+
}
|
|
5941
|
+
case "scout_result": {
|
|
5942
|
+
const preview = e.selected.slice(0, 4).join(", ");
|
|
5943
|
+
const more = e.selected.length > 4 ? "\u2026" : "";
|
|
5944
|
+
sessionSpinner.current?.succeed(`scout picked ${e.selected.length} files: ${preview}${more}`);
|
|
5945
|
+
sessionSpinner.current = spinner("drafting\u2026");
|
|
4884
5946
|
break;
|
|
4885
5947
|
}
|
|
4886
5948
|
case "assistant_text":
|
|
@@ -4888,9 +5950,11 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4888
5950
|
sessionSpinner.current = spinner("thinking\u2026");
|
|
4889
5951
|
break;
|
|
4890
5952
|
case "rate_limit":
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
5953
|
+
if (e.phase === "retrying") {
|
|
5954
|
+
sessionSpinner.current?.stop();
|
|
5955
|
+
warning(`${e.provider} rate limit hit \u2014 retrying in ${e.waitSec}s`);
|
|
5956
|
+
sessionSpinner.current = spinner("waiting for rate limit to reset\u2026");
|
|
5957
|
+
}
|
|
4894
5958
|
break;
|
|
4895
5959
|
case "usage":
|
|
4896
5960
|
break;
|
|
@@ -4914,9 +5978,9 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4914
5978
|
try {
|
|
4915
5979
|
result = await runPlanner({
|
|
4916
5980
|
root: paths.root,
|
|
4917
|
-
provider,
|
|
4918
5981
|
model,
|
|
4919
|
-
|
|
5982
|
+
provider: planner.provider,
|
|
5983
|
+
modelId,
|
|
4920
5984
|
systemPrompt,
|
|
4921
5985
|
userPrompt,
|
|
4922
5986
|
budget,
|
|
@@ -4924,7 +5988,19 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4924
5988
|
cacheEnabled,
|
|
4925
5989
|
decideOnLimit,
|
|
4926
5990
|
events: bus,
|
|
4927
|
-
runId
|
|
5991
|
+
runId,
|
|
5992
|
+
stages: {
|
|
5993
|
+
scout: {
|
|
5994
|
+
enabled: scoutEnabled,
|
|
5995
|
+
model: scoutModelHandle,
|
|
5996
|
+
modelId: scoutModelId,
|
|
5997
|
+
maxFiles: maxScoutFiles,
|
|
5998
|
+
maxOutputTokens: planner.stages?.scout?.maxOutputTokens ?? 2048
|
|
5999
|
+
}
|
|
6000
|
+
},
|
|
6001
|
+
validation: { enabled: validationEnabled, strict: strictValidation },
|
|
6002
|
+
toolsEnabled: planner.tools,
|
|
6003
|
+
scoutSystemPrompt
|
|
4928
6004
|
});
|
|
4929
6005
|
} finally {
|
|
4930
6006
|
unsubscribe();
|
|
@@ -4937,17 +6013,22 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4937
6013
|
}
|
|
4938
6014
|
const snap = budget.snapshot();
|
|
4939
6015
|
const elapsedMs = Date.now() - startedAt;
|
|
4940
|
-
const
|
|
6016
|
+
const issueCount = result.validation?.issues.length ?? 0;
|
|
6017
|
+
const issuesByKind = summariseIssuesByKind(result.validation?.issues ?? []);
|
|
6018
|
+
const validationBlocks = strictValidation && validationEnabled && issueCount > 0;
|
|
6019
|
+
const success2 = result.finishedNormally && !result.timedOut && !result.userCancelled && !validationBlocks;
|
|
4941
6020
|
const header = buildMetadataHeader({
|
|
4942
6021
|
provider: planner.provider,
|
|
4943
|
-
model,
|
|
6022
|
+
model: modelId,
|
|
4944
6023
|
reads: snap.reads,
|
|
4945
6024
|
bytes: snap.bytes,
|
|
4946
6025
|
inputTokens: snap.usage.inputTokens,
|
|
4947
6026
|
outputTokens: snap.usage.outputTokens,
|
|
4948
6027
|
durationMs: elapsedMs,
|
|
4949
|
-
|
|
4950
|
-
|
|
6028
|
+
planStatus: success2 ? void 0 : "partial",
|
|
6029
|
+
scoutEnabled,
|
|
6030
|
+
validationEnabled,
|
|
6031
|
+
validationIssueCount: issueCount
|
|
4951
6032
|
});
|
|
4952
6033
|
const { planFile, sequenceNumber, overwrote } = writePlanFile({
|
|
4953
6034
|
paths,
|
|
@@ -4957,18 +6038,18 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4957
6038
|
metadataHeader: header,
|
|
4958
6039
|
partial: !success2
|
|
4959
6040
|
});
|
|
4960
|
-
const relPlan =
|
|
6041
|
+
const relPlan = path23.relative(paths.root, planFile);
|
|
4961
6042
|
try {
|
|
4962
6043
|
await writeLastRun(paths, {
|
|
4963
6044
|
stats: result.stats,
|
|
4964
6045
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4965
6046
|
provider: planner.provider,
|
|
4966
|
-
model
|
|
6047
|
+
model: modelId
|
|
4967
6048
|
});
|
|
4968
6049
|
await appendRun(paths, {
|
|
4969
6050
|
runId,
|
|
4970
6051
|
provider: planner.provider,
|
|
4971
|
-
model,
|
|
6052
|
+
model: modelId,
|
|
4972
6053
|
feature: story.feature,
|
|
4973
6054
|
storyId: story.id,
|
|
4974
6055
|
startedAt: new Date(startedAt).toISOString(),
|
|
@@ -4978,7 +6059,19 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4978
6059
|
planFile: relPlan,
|
|
4979
6060
|
stats: result.stats,
|
|
4980
6061
|
cacheEnabled,
|
|
4981
|
-
durationMs: elapsedMs
|
|
6062
|
+
durationMs: elapsedMs,
|
|
6063
|
+
scout: {
|
|
6064
|
+
enabled: scoutEnabled,
|
|
6065
|
+
selectedCount: result.scout?.selected.length,
|
|
6066
|
+
tokensUsed: result.scout?.tokensUsed,
|
|
6067
|
+
durationMs: result.scout?.durationMs
|
|
6068
|
+
},
|
|
6069
|
+
validation: {
|
|
6070
|
+
enabled: validationEnabled,
|
|
6071
|
+
issuesCount: issueCount,
|
|
6072
|
+
issuesByKind,
|
|
6073
|
+
durationMs: result.validation?.durationMs
|
|
6074
|
+
}
|
|
4982
6075
|
});
|
|
4983
6076
|
} catch {
|
|
4984
6077
|
}
|
|
@@ -4986,11 +6079,19 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
4986
6079
|
summaryBox(success2 ? " plan generated " : " plan saved (incomplete) ", [
|
|
4987
6080
|
{ key: "file", value: relPlan },
|
|
4988
6081
|
{ key: "nn", value: String(sequenceNumber).padStart(2, "0") },
|
|
4989
|
-
{ key: "model", value: `${planner.provider}/${
|
|
6082
|
+
{ key: "model", value: `${planner.provider}/${modelId}` },
|
|
6083
|
+
{
|
|
6084
|
+
key: "scout",
|
|
6085
|
+
value: scoutEnabled ? `${result.scout?.selected.length ?? 0} files \xB7 ${result.scout?.tokensUsed ?? 0} tok` : "disabled"
|
|
6086
|
+
},
|
|
6087
|
+
{
|
|
6088
|
+
key: "validation",
|
|
6089
|
+
value: validationEnabled ? `${issueCount} issue(s)` : "disabled"
|
|
6090
|
+
},
|
|
4990
6091
|
{ key: "reads", value: `${snap.reads} files \xB7 ${(snap.bytes / 1024).toFixed(1)} KB` },
|
|
4991
6092
|
{
|
|
4992
6093
|
key: "tokens",
|
|
4993
|
-
value: `${snap.usage.inputTokens} in \xB7 ${snap.usage.outputTokens} out
|
|
6094
|
+
value: `${snap.usage.inputTokens} in \xB7 ${snap.usage.outputTokens} out`
|
|
4994
6095
|
},
|
|
4995
6096
|
{ key: "cache", value: formatPlannerCacheLine({ cacheEnabled, stats: result.stats }) },
|
|
4996
6097
|
{
|
|
@@ -5000,25 +6101,45 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
5000
6101
|
{ key: "time", value: `${Math.round(elapsedMs / 1e3)}s` },
|
|
5001
6102
|
{
|
|
5002
6103
|
key: "action",
|
|
5003
|
-
value: success2 ? overwrote ? "overwrote existing plan" : "new plan" : "partial plan (review limits)"
|
|
6104
|
+
value: success2 ? overwrote ? "overwrote existing plan" : "new plan" : validationBlocks ? "partial plan (validation issues \u2014 review or use --no-validation)" : "partial plan (review limits)"
|
|
5004
6105
|
}
|
|
5005
6106
|
]);
|
|
6107
|
+
if (issueCount > 0 && validationEnabled) {
|
|
6108
|
+
blank();
|
|
6109
|
+
warning(`Validation reported ${issueCount} issue(s) (heuristic; first 10 shown):`);
|
|
6110
|
+
for (const iss of result.validation.issues.slice(0, 10)) {
|
|
6111
|
+
info(
|
|
6112
|
+
` [${iss.severity}] ${iss.kind}${iss.path ? ` ${iss.path}` : ""} \u2014 ${iss.detail}`
|
|
6113
|
+
);
|
|
6114
|
+
}
|
|
6115
|
+
if (issueCount > 10) {
|
|
6116
|
+
info(` (+${issueCount - 10} more \u2014 see metadata header issues=\u2026)`);
|
|
6117
|
+
}
|
|
6118
|
+
if (issueCount > 50) {
|
|
6119
|
+
warning("Large issue count may mean the validator is misfiring; try `squad new-plan --no-validation` to compare.");
|
|
6120
|
+
}
|
|
6121
|
+
}
|
|
5006
6122
|
if (!success2) {
|
|
5007
|
-
|
|
6123
|
+
if (!validationBlocks || !result.finishedNormally) {
|
|
6124
|
+
printPlannerRunIncompleteSummary(result, relPlan);
|
|
6125
|
+
} else {
|
|
6126
|
+
blank();
|
|
6127
|
+
warning("Plan saved as partial because strict validation reported issues.");
|
|
6128
|
+
}
|
|
5008
6129
|
throw new SquadExit(2);
|
|
5009
6130
|
}
|
|
5010
6131
|
if (result.budgetExhausted) {
|
|
5011
6132
|
info("Note: file-read budget was reached; the model finalised without further reads.");
|
|
5012
6133
|
}
|
|
5013
6134
|
blank();
|
|
5014
|
-
info(`next \u2192 open a new agent chat and attach only ${
|
|
6135
|
+
info(`next \u2192 open a new agent chat and attach only ${path23.basename(planFile)}`);
|
|
5015
6136
|
}
|
|
5016
6137
|
async function emitCopyPrompt(story, paths, config2, clipboard) {
|
|
5017
6138
|
const intakeContent = readFile(story.intakePath);
|
|
5018
6139
|
const composed = buildCopyPlanPromptMarkdown(config2, intakeContent);
|
|
5019
|
-
const promptFile =
|
|
5020
|
-
|
|
5021
|
-
const relPromptFile =
|
|
6140
|
+
const promptFile = path23.join(paths.squadDir, ".last-copy-prompt.md");
|
|
6141
|
+
fs31.writeFileSync(promptFile, composed, "utf8");
|
|
6142
|
+
const relPromptFile = path23.relative(paths.root, promptFile);
|
|
5022
6143
|
const bytes = Buffer.byteLength(composed, "utf8");
|
|
5023
6144
|
const estTokens = Math.round(bytes / 4);
|
|
5024
6145
|
const clipResult = clipboard ? await copyToClipboard(composed) : { ok: false, reason: "clipboard disabled (--no-clipboard)" };
|
|
@@ -5076,17 +6197,17 @@ function printModelBanner(config2) {
|
|
|
5076
6197
|
}
|
|
5077
6198
|
|
|
5078
6199
|
// src/commands/status.ts
|
|
5079
|
-
import
|
|
6200
|
+
import fs32 from "fs";
|
|
5080
6201
|
var kw = 28;
|
|
5081
6202
|
function probeTrackerCredential(config2, overlay) {
|
|
5082
|
-
const { client, error
|
|
6203
|
+
const { client, error } = clientFor(config2, overlay);
|
|
5083
6204
|
if (client) {
|
|
5084
6205
|
const fromEnv = config2.tracker.type === "jira" && Boolean(process.env.JIRA_API_TOKEN || process.env.SQUAD_TRACKER_API_KEY) || config2.tracker.type === "azure" && Boolean(process.env.AZURE_DEVOPS_PAT || process.env.SQUAD_TRACKER_API_KEY);
|
|
5085
6206
|
return { state: "present", detail: fromEnv ? "env" : "secrets.yaml" };
|
|
5086
6207
|
}
|
|
5087
6208
|
return {
|
|
5088
6209
|
state: "missing",
|
|
5089
|
-
hint:
|
|
6210
|
+
hint: error?.detail ?? "Run `squad init` to enter credentials."
|
|
5090
6211
|
};
|
|
5091
6212
|
}
|
|
5092
6213
|
function formatCredentialStatus(s) {
|
|
@@ -5102,7 +6223,7 @@ async function runStatus() {
|
|
|
5102
6223
|
const scan = scanPlans(paths.plansDir);
|
|
5103
6224
|
const storyCount = listStories(paths).length;
|
|
5104
6225
|
const planCount = scan.usedNumbers.length;
|
|
5105
|
-
const secrets =
|
|
6226
|
+
const secrets = fs32.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
5106
6227
|
const overlay = overlayTrackerEnv(secrets);
|
|
5107
6228
|
banner();
|
|
5108
6229
|
step("squad-kit status");
|
|
@@ -5156,13 +6277,13 @@ function buildTrackerLine(config2, overlay) {
|
|
|
5156
6277
|
}
|
|
5157
6278
|
|
|
5158
6279
|
// src/commands/list.ts
|
|
5159
|
-
import
|
|
6280
|
+
import path24 from "path";
|
|
5160
6281
|
async function runList(opts) {
|
|
5161
6282
|
const root = requireSquadRoot();
|
|
5162
6283
|
const paths = buildPaths(root);
|
|
5163
6284
|
const records = listStories(paths, { feature: opts.feature });
|
|
5164
6285
|
const rows = records.map((r) => {
|
|
5165
|
-
const planAbs = r.planFile ?
|
|
6286
|
+
const planAbs = r.planFile ? path24.join(paths.plansDir, r.feature, r.planFile) : void 0;
|
|
5166
6287
|
const meta = planAbs ? readPlanMetadata(planAbs) : void 0;
|
|
5167
6288
|
return {
|
|
5168
6289
|
feature: r.feature,
|
|
@@ -5201,8 +6322,8 @@ function stripAnsi(s) {
|
|
|
5201
6322
|
}
|
|
5202
6323
|
|
|
5203
6324
|
// src/commands/tracker-link.ts
|
|
5204
|
-
import
|
|
5205
|
-
import
|
|
6325
|
+
import fs33 from "fs";
|
|
6326
|
+
import path25 from "path";
|
|
5206
6327
|
import { input as input4, select as select7 } from "@inquirer/prompts";
|
|
5207
6328
|
async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
5208
6329
|
const root = requireSquadRoot();
|
|
@@ -5249,17 +6370,17 @@ async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
|
5249
6370
|
`Invalid tracker id "${resolvedTrackerId}" for tracker type "${config2.tracker.type}". Run \`squad config show\` to review the expected format, or \`squad config set tracker\` if the type is wrong.`
|
|
5250
6371
|
);
|
|
5251
6372
|
}
|
|
5252
|
-
const resolved =
|
|
5253
|
-
const intakeFile = resolved.endsWith("intake.md") ? resolved :
|
|
5254
|
-
if (!
|
|
6373
|
+
const resolved = path25.resolve(resolvedStoryPath);
|
|
6374
|
+
const intakeFile = resolved.endsWith("intake.md") ? resolved : path25.join(resolved, "intake.md");
|
|
6375
|
+
if (!fs33.existsSync(intakeFile)) {
|
|
5255
6376
|
throw new Error(
|
|
5256
6377
|
`intake.md not found at ${intakeFile}. Run \`squad list\` to see valid paths, or \`squad new-story\` to create a story first.`
|
|
5257
6378
|
);
|
|
5258
6379
|
}
|
|
5259
|
-
const content =
|
|
6380
|
+
const content = fs33.readFileSync(intakeFile, "utf8");
|
|
5260
6381
|
const updated = upsertTrackerId(content, resolvedTrackerId);
|
|
5261
|
-
|
|
5262
|
-
success(`Linked tracker id ${resolvedTrackerId} \u2192 ${
|
|
6382
|
+
fs33.writeFileSync(intakeFile, updated, "utf8");
|
|
6383
|
+
success(`Linked tracker id ${resolvedTrackerId} \u2192 ${path25.relative(root, intakeFile)}`);
|
|
5263
6384
|
blank();
|
|
5264
6385
|
step("Next:");
|
|
5265
6386
|
info("1) Review the intake \u2014 the Source block at the top now carries the tracker id.");
|
|
@@ -5277,39 +6398,39 @@ function upsertTrackerId(content, id) {
|
|
|
5277
6398
|
}
|
|
5278
6399
|
|
|
5279
6400
|
// src/commands/doctor-engine.ts
|
|
5280
|
-
import
|
|
5281
|
-
import
|
|
6401
|
+
import fs34 from "fs";
|
|
6402
|
+
import path26 from "path";
|
|
5282
6403
|
async function gatherContext(paths) {
|
|
5283
6404
|
const ctx = { hasLegacyPromptsDir: false };
|
|
5284
6405
|
try {
|
|
5285
6406
|
ctx.config = loadConfig(paths.configFile);
|
|
5286
|
-
} catch (
|
|
5287
|
-
ctx.configError =
|
|
6407
|
+
} catch (err2) {
|
|
6408
|
+
ctx.configError = err2;
|
|
5288
6409
|
}
|
|
5289
6410
|
try {
|
|
5290
|
-
ctx.secrets =
|
|
5291
|
-
} catch (
|
|
5292
|
-
ctx.secretsError =
|
|
6411
|
+
ctx.secrets = fs34.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
6412
|
+
} catch (err2) {
|
|
6413
|
+
ctx.secretsError = err2;
|
|
5293
6414
|
}
|
|
5294
|
-
ctx.hasLegacyPromptsDir =
|
|
6415
|
+
ctx.hasLegacyPromptsDir = fs34.existsSync(paths.promptsDir);
|
|
5295
6416
|
return ctx;
|
|
5296
6417
|
}
|
|
5297
6418
|
function gitignoreHasManagedBlock(repoRoot) {
|
|
5298
|
-
const gitignore =
|
|
5299
|
-
return
|
|
6419
|
+
const gitignore = path26.join(repoRoot, ".gitignore");
|
|
6420
|
+
return fs34.existsSync(gitignore) && fs34.readFileSync(gitignore, "utf8").includes(".squad/secrets.yaml");
|
|
5300
6421
|
}
|
|
5301
6422
|
function gitignoreHasTrashPattern(repoRoot) {
|
|
5302
|
-
const gitignore =
|
|
5303
|
-
return
|
|
6423
|
+
const gitignore = path26.join(repoRoot, ".gitignore");
|
|
6424
|
+
return fs34.existsSync(gitignore) && fs34.readFileSync(gitignore, "utf8").includes(SQUAD_TRASH_PATTERN);
|
|
5304
6425
|
}
|
|
5305
6426
|
async function checkDirStructure(paths, _ctx, fix) {
|
|
5306
|
-
const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !
|
|
6427
|
+
const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !fs34.existsSync(p));
|
|
5307
6428
|
if (need.length === 0) {
|
|
5308
6429
|
return { id: "dirs", name: ".squad/ directory structure", status: "ok" };
|
|
5309
6430
|
}
|
|
5310
6431
|
if (fix) {
|
|
5311
6432
|
for (const p of need) {
|
|
5312
|
-
|
|
6433
|
+
fs34.mkdirSync(p, { recursive: true });
|
|
5313
6434
|
}
|
|
5314
6435
|
return { id: "dirs", name: ".squad/ directory structure", status: "ok", detail: "repaired" };
|
|
5315
6436
|
}
|
|
@@ -5317,7 +6438,7 @@ async function checkDirStructure(paths, _ctx, fix) {
|
|
|
5317
6438
|
id: "dirs",
|
|
5318
6439
|
name: ".squad/ directory structure",
|
|
5319
6440
|
status: "warn",
|
|
5320
|
-
detail: `missing: ${need.map((p) =>
|
|
6441
|
+
detail: `missing: ${need.map((p) => path26.relative(paths.root, p)).join(", ")}`,
|
|
5321
6442
|
fixable: true,
|
|
5322
6443
|
fixHint: "squad doctor --fix"
|
|
5323
6444
|
};
|
|
@@ -5372,15 +6493,15 @@ async function checkSecretsPermissions(paths, _ctx, fix) {
|
|
|
5372
6493
|
if (process.platform === "win32") {
|
|
5373
6494
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "skip", detail: "Windows" };
|
|
5374
6495
|
}
|
|
5375
|
-
if (!
|
|
6496
|
+
if (!fs34.existsSync(paths.secretsFile)) {
|
|
5376
6497
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "not present" };
|
|
5377
6498
|
}
|
|
5378
|
-
const mode =
|
|
6499
|
+
const mode = fs34.statSync(paths.secretsFile).mode & 511;
|
|
5379
6500
|
if (mode === 384) {
|
|
5380
6501
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok" };
|
|
5381
6502
|
}
|
|
5382
6503
|
if (fix) {
|
|
5383
|
-
|
|
6504
|
+
fs34.chmodSync(paths.secretsFile, 384);
|
|
5384
6505
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "repaired" };
|
|
5385
6506
|
}
|
|
5386
6507
|
return {
|
|
@@ -5401,7 +6522,7 @@ async function checkSecretsParseable(_paths, ctx) {
|
|
|
5401
6522
|
detail: ctx.secretsError.message
|
|
5402
6523
|
};
|
|
5403
6524
|
}
|
|
5404
|
-
if (!
|
|
6525
|
+
if (!fs34.existsSync(_paths.secretsFile)) {
|
|
5405
6526
|
return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok", detail: "not present" };
|
|
5406
6527
|
}
|
|
5407
6528
|
return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok" };
|
|
@@ -5461,14 +6582,6 @@ async function checkPlannerConfig(_paths, ctx) {
|
|
|
5461
6582
|
detail: "planner budget limits must be > 0"
|
|
5462
6583
|
};
|
|
5463
6584
|
}
|
|
5464
|
-
if (p.budget.maxCostUsd !== void 0 && p.budget.maxCostUsd <= 0) {
|
|
5465
|
-
return {
|
|
5466
|
-
id: "planner-config",
|
|
5467
|
-
name: "planner configuration",
|
|
5468
|
-
status: "fail",
|
|
5469
|
-
detail: "planner.budget.maxCostUsd must be > 0 when set"
|
|
5470
|
-
};
|
|
5471
|
-
}
|
|
5472
6585
|
return { id: "planner-config", name: "planner configuration", status: "ok" };
|
|
5473
6586
|
}
|
|
5474
6587
|
async function checkPlannerCredential(_paths, ctx) {
|
|
@@ -5543,12 +6656,12 @@ async function checkPlannerModel(_paths, ctx) {
|
|
|
5543
6656
|
status: "ok",
|
|
5544
6657
|
detail: `${model} (${provider})`
|
|
5545
6658
|
};
|
|
5546
|
-
} catch (
|
|
6659
|
+
} catch (err2) {
|
|
5547
6660
|
return {
|
|
5548
6661
|
id: "planner-model",
|
|
5549
6662
|
name: "planner model resolves at provider",
|
|
5550
6663
|
status: "warn",
|
|
5551
|
-
detail:
|
|
6664
|
+
detail: err2.message
|
|
5552
6665
|
};
|
|
5553
6666
|
}
|
|
5554
6667
|
}
|
|
@@ -5681,8 +6794,8 @@ async function checkTrackerConfig(_paths, ctx) {
|
|
|
5681
6794
|
}
|
|
5682
6795
|
return { id: "tracker-config", name: "tracker configuration", status: "ok" };
|
|
5683
6796
|
}
|
|
5684
|
-
function formatClientError(
|
|
5685
|
-
return `${
|
|
6797
|
+
function formatClientError(err2) {
|
|
6798
|
+
return `${err2.message} ${err2.detail}`.trim();
|
|
5686
6799
|
}
|
|
5687
6800
|
async function checkTrackerCredential(_paths, ctx) {
|
|
5688
6801
|
if (!ctx.config) {
|
|
@@ -5693,13 +6806,13 @@ async function checkTrackerCredential(_paths, ctx) {
|
|
|
5693
6806
|
}
|
|
5694
6807
|
const secrets = ctx.secrets ?? {};
|
|
5695
6808
|
const overlay = overlayTrackerEnv(secrets);
|
|
5696
|
-
const { client, error
|
|
5697
|
-
if (
|
|
6809
|
+
const { client, error } = clientFor(ctx.config, overlay);
|
|
6810
|
+
if (error) {
|
|
5698
6811
|
return {
|
|
5699
6812
|
id: "tracker-cred",
|
|
5700
6813
|
name: "tracker credential resolves",
|
|
5701
6814
|
status: "fail",
|
|
5702
|
-
detail: formatClientError(
|
|
6815
|
+
detail: formatClientError(error)
|
|
5703
6816
|
};
|
|
5704
6817
|
}
|
|
5705
6818
|
if (!client) {
|
|
@@ -5718,8 +6831,8 @@ async function checkTrackerConnectivity(paths, ctx) {
|
|
|
5718
6831
|
}
|
|
5719
6832
|
const secrets = ctx.secrets ?? {};
|
|
5720
6833
|
const overlay = overlayTrackerEnv(secrets);
|
|
5721
|
-
const { client, error
|
|
5722
|
-
if (
|
|
6834
|
+
const { client, error } = clientFor(ctx.config, overlay);
|
|
6835
|
+
if (error || !client) {
|
|
5723
6836
|
return { id: "tracker-live", name: "tracker connectivity", status: "skip", detail: "no credential" };
|
|
5724
6837
|
}
|
|
5725
6838
|
if (client.name === "jira") {
|
|
@@ -5762,12 +6875,12 @@ async function checkTrackerConnectivity(paths, ctx) {
|
|
|
5762
6875
|
async function guardCheck(checkName, fn) {
|
|
5763
6876
|
try {
|
|
5764
6877
|
return await fn();
|
|
5765
|
-
} catch (
|
|
6878
|
+
} catch (err2) {
|
|
5766
6879
|
return {
|
|
5767
6880
|
id: "unexpected",
|
|
5768
6881
|
name: checkName,
|
|
5769
6882
|
status: "fail",
|
|
5770
|
-
detail:
|
|
6883
|
+
detail: err2.message
|
|
5771
6884
|
};
|
|
5772
6885
|
}
|
|
5773
6886
|
}
|
|
@@ -5895,23 +7008,23 @@ async function runDoctor(opts) {
|
|
|
5895
7008
|
}
|
|
5896
7009
|
|
|
5897
7010
|
// src/commands/migrate.ts
|
|
5898
|
-
import
|
|
5899
|
-
import
|
|
7011
|
+
import fs35 from "fs";
|
|
7012
|
+
import path27 from "path";
|
|
5900
7013
|
import { confirm as confirm7 } from "@inquirer/prompts";
|
|
5901
7014
|
var LEGACY_PROMPTS_MIGRATION = {
|
|
5902
7015
|
id: "legacy-prompts",
|
|
5903
7016
|
name: "Remove legacy .squad/prompts/",
|
|
5904
|
-
isApplied: (paths) => !
|
|
5905
|
-
describe: (paths) => `delete ${
|
|
7017
|
+
isApplied: (paths) => !fs35.existsSync(paths.promptsDir),
|
|
7018
|
+
describe: (paths) => `delete ${path27.relative(paths.root, paths.promptsDir)} (prompts are now bundled in squad-kit)`,
|
|
5906
7019
|
apply: (paths) => {
|
|
5907
7020
|
const target = paths.promptsDir;
|
|
5908
|
-
if (!target.includes(
|
|
7021
|
+
if (!target.includes(path27.sep + ".squad" + path27.sep + "prompts")) {
|
|
5909
7022
|
throw new Error(
|
|
5910
7023
|
`Refusing to delete suspicious path: ${target}. This is a safety check \u2014 run \`squad doctor\` if .squad/ paths look wrong.`
|
|
5911
7024
|
);
|
|
5912
7025
|
}
|
|
5913
|
-
if (
|
|
5914
|
-
|
|
7026
|
+
if (fs35.existsSync(target)) {
|
|
7027
|
+
fs35.rmSync(target, { recursive: true, force: true });
|
|
5915
7028
|
}
|
|
5916
7029
|
}
|
|
5917
7030
|
};
|
|
@@ -5919,8 +7032,8 @@ var GITIGNORE_MIGRATION = {
|
|
|
5919
7032
|
id: "gitignore",
|
|
5920
7033
|
name: "Ensure .gitignore managed block",
|
|
5921
7034
|
isApplied: (paths) => {
|
|
5922
|
-
const f =
|
|
5923
|
-
return
|
|
7035
|
+
const f = path27.join(paths.root, ".gitignore");
|
|
7036
|
+
return fs35.existsSync(f) && fs35.readFileSync(f, "utf8").includes(".squad/secrets.yaml");
|
|
5924
7037
|
},
|
|
5925
7038
|
describe: () => "add squad-kit managed block to .gitignore",
|
|
5926
7039
|
apply: (paths) => {
|
|
@@ -5932,23 +7045,23 @@ var SECRETS_PERMS_MIGRATION = {
|
|
|
5932
7045
|
name: "Tighten .squad/secrets.yaml permissions",
|
|
5933
7046
|
isApplied: (paths) => {
|
|
5934
7047
|
if (process.platform === "win32") return true;
|
|
5935
|
-
if (!
|
|
5936
|
-
const st =
|
|
7048
|
+
if (!fs35.existsSync(paths.secretsFile)) return true;
|
|
7049
|
+
const st = fs35.statSync(paths.secretsFile);
|
|
5937
7050
|
return (st.mode & 511) === 384;
|
|
5938
7051
|
},
|
|
5939
|
-
describe: (paths) => `chmod 0600 ${
|
|
7052
|
+
describe: (paths) => `chmod 0600 ${path27.relative(paths.root, paths.secretsFile)}`,
|
|
5940
7053
|
apply: (paths) => {
|
|
5941
7054
|
if (process.platform === "win32") return;
|
|
5942
|
-
if (
|
|
7055
|
+
if (fs35.existsSync(paths.secretsFile)) fs35.chmodSync(paths.secretsFile, 384);
|
|
5943
7056
|
}
|
|
5944
7057
|
};
|
|
5945
7058
|
var PLANNER_BUDGET_DEFAULTS_MIGRATION = {
|
|
5946
7059
|
id: "planner-budget-defaults",
|
|
5947
7060
|
name: "Backfill planner.budget defaults in config.yaml",
|
|
5948
7061
|
isApplied: (paths) => {
|
|
5949
|
-
if (!
|
|
7062
|
+
if (!fs35.existsSync(paths.configFile)) return true;
|
|
5950
7063
|
try {
|
|
5951
|
-
const raw =
|
|
7064
|
+
const raw = fs35.readFileSync(paths.configFile, "utf8");
|
|
5952
7065
|
if (!/^\s*planner:/m.test(raw)) return true;
|
|
5953
7066
|
if (/^\s*budget:/m.test(raw)) return true;
|
|
5954
7067
|
const cfg = loadConfig(paths.configFile);
|
|
@@ -5967,9 +7080,9 @@ var CONFIG_NORMALIZE_MIGRATION = {
|
|
|
5967
7080
|
id: "config-normalize",
|
|
5968
7081
|
name: "Normalise .squad/config.yaml formatting",
|
|
5969
7082
|
isApplied: (paths) => {
|
|
5970
|
-
if (!
|
|
7083
|
+
if (!fs35.existsSync(paths.configFile)) return true;
|
|
5971
7084
|
try {
|
|
5972
|
-
const raw =
|
|
7085
|
+
const raw = fs35.readFileSync(paths.configFile, "utf8");
|
|
5973
7086
|
const cfg = loadConfig(paths.configFile);
|
|
5974
7087
|
return raw.trim() === serializeConfig(cfg).trim();
|
|
5975
7088
|
} catch {
|
|
@@ -5978,7 +7091,7 @@ var CONFIG_NORMALIZE_MIGRATION = {
|
|
|
5978
7091
|
},
|
|
5979
7092
|
describe: () => "rewrite config.yaml with canonical YAML formatting (drops comments)",
|
|
5980
7093
|
apply: (paths) => {
|
|
5981
|
-
if (!
|
|
7094
|
+
if (!fs35.existsSync(paths.configFile)) return;
|
|
5982
7095
|
const cfg = loadConfig(paths.configFile);
|
|
5983
7096
|
saveConfig(paths.configFile, cfg);
|
|
5984
7097
|
}
|
|
@@ -6039,8 +7152,8 @@ async function runMigrate(opts) {
|
|
|
6039
7152
|
await Promise.resolve(m.apply(paths));
|
|
6040
7153
|
spin.succeed(`${m.name} \u2014 applied`);
|
|
6041
7154
|
applied++;
|
|
6042
|
-
} catch (
|
|
6043
|
-
spin.fail(`${m.name} \u2014 failed: ${
|
|
7155
|
+
} catch (err2) {
|
|
7156
|
+
spin.fail(`${m.name} \u2014 failed: ${err2.message}`);
|
|
6044
7157
|
process.exit(1);
|
|
6045
7158
|
}
|
|
6046
7159
|
}
|
|
@@ -6059,8 +7172,8 @@ import process3 from "process";
|
|
|
6059
7172
|
import { confirm as confirm8 } from "@inquirer/prompts";
|
|
6060
7173
|
|
|
6061
7174
|
// src/core/package-info.ts
|
|
6062
|
-
import
|
|
6063
|
-
import
|
|
7175
|
+
import fs36 from "fs";
|
|
7176
|
+
import path28 from "path";
|
|
6064
7177
|
function changelogBlobUrl(repositoryUrl, version) {
|
|
6065
7178
|
let base = repositoryUrl;
|
|
6066
7179
|
if (!base) base = "https://github.com/AzmSquad/squad-kit";
|
|
@@ -6071,9 +7184,9 @@ function changelogBlobUrl(repositoryUrl, version) {
|
|
|
6071
7184
|
}
|
|
6072
7185
|
function readInstalledPackage() {
|
|
6073
7186
|
const root = packageRoot();
|
|
6074
|
-
const pkgFile =
|
|
6075
|
-
const pkg = JSON.parse(
|
|
6076
|
-
const isDevInstall =
|
|
7187
|
+
const pkgFile = path28.join(root, "package.json");
|
|
7188
|
+
const pkg = JSON.parse(fs36.readFileSync(pkgFile, "utf8"));
|
|
7189
|
+
const isDevInstall = fs36.existsSync(path28.join(root, "src"));
|
|
6077
7190
|
return {
|
|
6078
7191
|
name: pkg.name,
|
|
6079
7192
|
version: pkg.version,
|
|
@@ -6113,7 +7226,7 @@ function isNewer(a, b) {
|
|
|
6113
7226
|
}
|
|
6114
7227
|
|
|
6115
7228
|
// src/core/package-manager.ts
|
|
6116
|
-
import
|
|
7229
|
+
import path29 from "path";
|
|
6117
7230
|
import process2 from "process";
|
|
6118
7231
|
function detectPackageManager(installRoot) {
|
|
6119
7232
|
const ua = process2.env.npm_config_user_agent;
|
|
@@ -6124,11 +7237,11 @@ function detectPackageManager(installRoot) {
|
|
|
6124
7237
|
if (ua.startsWith("npm/")) return { pm: "npm", reason: "npm_config_user_agent" };
|
|
6125
7238
|
}
|
|
6126
7239
|
const root = installRoot.toLowerCase();
|
|
6127
|
-
if (root.includes(`${
|
|
7240
|
+
if (root.includes(`${path29.sep}pnpm${path29.sep}`) || root.includes(`${path29.sep}.pnpm${path29.sep}`))
|
|
6128
7241
|
return { pm: "pnpm", reason: `install path contains pnpm` };
|
|
6129
|
-
if (root.includes(`${
|
|
7242
|
+
if (root.includes(`${path29.sep}.yarn${path29.sep}`) || root.includes(`${path29.sep}yarn${path29.sep}`))
|
|
6130
7243
|
return { pm: "yarn", reason: `install path contains yarn` };
|
|
6131
|
-
if (root.includes(`${
|
|
7244
|
+
if (root.includes(`${path29.sep}.bun${path29.sep}`) || root.includes(`${path29.sep}bun${path29.sep}`))
|
|
6132
7245
|
return { pm: "bun", reason: `install path contains bun` };
|
|
6133
7246
|
return { pm: "npm", reason: "default fallback" };
|
|
6134
7247
|
}
|
|
@@ -6162,9 +7275,9 @@ async function runUpgrade(opts) {
|
|
|
6162
7275
|
try {
|
|
6163
7276
|
const info2 = await fetchLatest(installed.name);
|
|
6164
7277
|
latest = info2.latest;
|
|
6165
|
-
} catch (
|
|
7278
|
+
} catch (err2) {
|
|
6166
7279
|
failure(
|
|
6167
|
-
`Could not reach the npm registry: ${
|
|
7280
|
+
`Could not reach the npm registry: ${err2.message}. Check your network, then run \`pnpm add -g squad-kit@latest\` (or \`npm i -g squad-kit@latest\`) manually.`
|
|
6168
7281
|
);
|
|
6169
7282
|
process3.exit(1);
|
|
6170
7283
|
}
|
|
@@ -6224,7 +7337,7 @@ function spawnInherit(cmd, args) {
|
|
|
6224
7337
|
return new Promise((resolve2, reject) => {
|
|
6225
7338
|
const child = spawn2(cmd, args, { stdio: "inherit", shell: process3.platform === "win32" });
|
|
6226
7339
|
child.on("exit", (code) => resolve2(code ?? 0));
|
|
6227
|
-
child.on("error", (
|
|
7340
|
+
child.on("error", (err2) => reject(err2));
|
|
6228
7341
|
});
|
|
6229
7342
|
}
|
|
6230
7343
|
|
|
@@ -6241,25 +7354,25 @@ import { cors } from "hono/cors";
|
|
|
6241
7354
|
import { secureHeaders } from "hono/secure-headers";
|
|
6242
7355
|
|
|
6243
7356
|
// src/console/api/stories.ts
|
|
6244
|
-
import
|
|
6245
|
-
import { z } from "zod";
|
|
7357
|
+
import fs37 from "fs";
|
|
7358
|
+
import { z as z5 } from "zod";
|
|
6246
7359
|
|
|
6247
7360
|
// src/utils/path-safety.ts
|
|
6248
|
-
import
|
|
7361
|
+
import path30 from "path";
|
|
6249
7362
|
function isInside(abs, parent) {
|
|
6250
|
-
const a =
|
|
6251
|
-
const p =
|
|
7363
|
+
const a = path30.resolve(abs) + path30.sep;
|
|
7364
|
+
const p = path30.resolve(parent) + path30.sep;
|
|
6252
7365
|
return a.startsWith(p);
|
|
6253
7366
|
}
|
|
6254
7367
|
|
|
6255
7368
|
// src/console/api/stories.ts
|
|
6256
|
-
var CreateBody =
|
|
6257
|
-
feature:
|
|
6258
|
-
title:
|
|
6259
|
-
trackerId:
|
|
7369
|
+
var CreateBody = z5.object({
|
|
7370
|
+
feature: z5.string().min(1).max(80),
|
|
7371
|
+
title: z5.string().max(200).optional(),
|
|
7372
|
+
trackerId: z5.string().max(80).optional()
|
|
6260
7373
|
});
|
|
6261
|
-
var PatchBody =
|
|
6262
|
-
intakeContent:
|
|
7374
|
+
var PatchBody = z5.object({
|
|
7375
|
+
intakeContent: z5.string().min(1).max(1e6)
|
|
6263
7376
|
});
|
|
6264
7377
|
function safeFeatureParam(feature) {
|
|
6265
7378
|
return feature.length > 0 && !feature.includes("/") && !feature.includes("..") && slugify(feature) === feature;
|
|
@@ -6288,7 +7401,7 @@ function mountStoriesApi(app, opts) {
|
|
|
6288
7401
|
if (!isInside(hit.intakePath, opts.paths.storiesDir)) return c.json({ error: "forbidden" }, 403);
|
|
6289
7402
|
let intakeContent = "";
|
|
6290
7403
|
try {
|
|
6291
|
-
intakeContent =
|
|
7404
|
+
intakeContent = fs37.readFileSync(hit.intakePath, "utf8");
|
|
6292
7405
|
} catch {
|
|
6293
7406
|
}
|
|
6294
7407
|
return c.json({
|
|
@@ -6332,7 +7445,7 @@ function mountStoriesApi(app, opts) {
|
|
|
6332
7445
|
const hit = stories.find((s) => s.id === id);
|
|
6333
7446
|
if (!hit) return c.json({ error: "not_found" }, 404);
|
|
6334
7447
|
if (!isInside(hit.intakePath, opts.paths.storiesDir)) return c.json({ error: "forbidden" }, 403);
|
|
6335
|
-
|
|
7448
|
+
fs37.writeFileSync(hit.intakePath, body.data.intakeContent, "utf8");
|
|
6336
7449
|
return c.json({ ok: true });
|
|
6337
7450
|
});
|
|
6338
7451
|
app.delete("/api/stories/:feature/:id", (c) => {
|
|
@@ -6349,14 +7462,14 @@ function mountStoriesApi(app, opts) {
|
|
|
6349
7462
|
}
|
|
6350
7463
|
|
|
6351
7464
|
// src/console/api/plans.ts
|
|
6352
|
-
import
|
|
6353
|
-
import
|
|
6354
|
-
import { z as
|
|
7465
|
+
import fs38 from "fs";
|
|
7466
|
+
import path31 from "path";
|
|
7467
|
+
import { z as z6 } from "zod";
|
|
6355
7468
|
import { diffLines } from "diff";
|
|
6356
|
-
var DiffQuery =
|
|
6357
|
-
feature:
|
|
6358
|
-
a:
|
|
6359
|
-
b:
|
|
7469
|
+
var DiffQuery = z6.object({
|
|
7470
|
+
feature: z6.string().min(1).max(80),
|
|
7471
|
+
a: z6.string().min(1).max(200),
|
|
7472
|
+
b: z6.string().min(1).max(200)
|
|
6360
7473
|
});
|
|
6361
7474
|
function safeFeatureParam2(feature) {
|
|
6362
7475
|
return feature.length > 0 && !feature.includes("/") && !feature.includes("..") && slugify(feature) === feature;
|
|
@@ -6365,11 +7478,11 @@ function mountPlansApi(app, opts) {
|
|
|
6365
7478
|
app.get("/api/plans", (c) => {
|
|
6366
7479
|
const out = [];
|
|
6367
7480
|
for (const feature of listPlanFeatureNames(opts.paths)) {
|
|
6368
|
-
const featureDir =
|
|
6369
|
-
if (!
|
|
6370
|
-
for (const entry of
|
|
7481
|
+
const featureDir = path31.join(opts.paths.plansDir, feature);
|
|
7482
|
+
if (!fs38.existsSync(featureDir)) continue;
|
|
7483
|
+
for (const entry of fs38.readdirSync(featureDir)) {
|
|
6371
7484
|
if (!entry.endsWith(".md") || entry === "00-index.md") continue;
|
|
6372
|
-
const abs =
|
|
7485
|
+
const abs = path31.join(featureDir, entry);
|
|
6373
7486
|
out.push({ feature, planFile: entry, metadata: readPlanMetadata(abs) });
|
|
6374
7487
|
}
|
|
6375
7488
|
}
|
|
@@ -6378,10 +7491,10 @@ function mountPlansApi(app, opts) {
|
|
|
6378
7491
|
app.get("/api/plans/:feature/:planFile", (c) => {
|
|
6379
7492
|
const { feature, planFile } = c.req.param();
|
|
6380
7493
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
6381
|
-
const abs =
|
|
7494
|
+
const abs = path31.resolve(path31.join(opts.paths.plansDir, feature, planFile));
|
|
6382
7495
|
if (!isInside(abs, opts.paths.plansDir)) return c.json({ error: "forbidden" }, 403);
|
|
6383
|
-
if (!
|
|
6384
|
-
const content =
|
|
7496
|
+
if (!fs38.existsSync(abs)) return c.json({ error: "not_found" }, 404);
|
|
7497
|
+
const content = fs38.readFileSync(abs, "utf8");
|
|
6385
7498
|
return c.json({
|
|
6386
7499
|
feature,
|
|
6387
7500
|
planFile,
|
|
@@ -6399,15 +7512,15 @@ function mountPlansApi(app, opts) {
|
|
|
6399
7512
|
if (!parse.success) return c.json({ error: "invalid_query", issues: parse.error.issues }, 400);
|
|
6400
7513
|
const { feature, a, b } = parse.data;
|
|
6401
7514
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
6402
|
-
const featureDir =
|
|
6403
|
-
const aAbs =
|
|
6404
|
-
const bAbs =
|
|
7515
|
+
const featureDir = path31.join(opts.paths.plansDir, feature);
|
|
7516
|
+
const aAbs = path31.join(featureDir, a);
|
|
7517
|
+
const bAbs = path31.join(featureDir, b);
|
|
6405
7518
|
if (!isInside(aAbs, opts.paths.plansDir) || !isInside(bAbs, opts.paths.plansDir)) {
|
|
6406
7519
|
return c.json({ error: "forbidden" }, 403);
|
|
6407
7520
|
}
|
|
6408
|
-
if (!
|
|
6409
|
-
const aText =
|
|
6410
|
-
const bText =
|
|
7521
|
+
if (!fs38.existsSync(aAbs) || !fs38.existsSync(bAbs)) return c.json({ error: "not_found" }, 404);
|
|
7522
|
+
const aText = fs38.readFileSync(aAbs, "utf8");
|
|
7523
|
+
const bText = fs38.readFileSync(bAbs, "utf8");
|
|
6411
7524
|
const changes = diffLines(aText, bText);
|
|
6412
7525
|
return c.json({ feature, a, b, changes });
|
|
6413
7526
|
});
|
|
@@ -6415,23 +7528,23 @@ function mountPlansApi(app, opts) {
|
|
|
6415
7528
|
const trash = c.req.query("trash") === "1";
|
|
6416
7529
|
const { feature, planFile } = c.req.param();
|
|
6417
7530
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
6418
|
-
const abs =
|
|
7531
|
+
const abs = path31.join(opts.paths.plansDir, feature, planFile);
|
|
6419
7532
|
if (!isInside(abs, opts.paths.plansDir)) return c.json({ error: "forbidden" }, 403);
|
|
6420
|
-
if (!
|
|
7533
|
+
if (!fs38.existsSync(abs)) return c.json({ error: "not_found" }, 404);
|
|
6421
7534
|
if (trash) {
|
|
6422
7535
|
const result = trashOrDelete([abs], opts.paths.trashDir, true);
|
|
6423
|
-
removeOverviewRowForPlanFile(
|
|
7536
|
+
removeOverviewRowForPlanFile(path31.join(opts.paths.plansDir, feature), planFile);
|
|
6424
7537
|
return c.json({ ok: true, trashed: result.trashed });
|
|
6425
7538
|
}
|
|
6426
|
-
|
|
6427
|
-
removeOverviewRowForPlanFile(
|
|
7539
|
+
fs38.rmSync(abs);
|
|
7540
|
+
removeOverviewRowForPlanFile(path31.join(opts.paths.plansDir, feature), planFile);
|
|
6428
7541
|
return c.json({ ok: true });
|
|
6429
7542
|
});
|
|
6430
7543
|
}
|
|
6431
7544
|
|
|
6432
7545
|
// src/console/api/config.ts
|
|
6433
|
-
import { z as
|
|
6434
|
-
var ConfigBody =
|
|
7546
|
+
import { z as z7 } from "zod";
|
|
7547
|
+
var ConfigBody = z7.object({}).passthrough();
|
|
6435
7548
|
function mountConfigApi(app, opts) {
|
|
6436
7549
|
app.get("/api/config", (c) => {
|
|
6437
7550
|
const config2 = loadConfig(opts.paths.configFile);
|
|
@@ -6443,8 +7556,8 @@ function mountConfigApi(app, opts) {
|
|
|
6443
7556
|
try {
|
|
6444
7557
|
saveConfig(opts.paths.configFile, body.data);
|
|
6445
7558
|
return c.json({ ok: true });
|
|
6446
|
-
} catch (
|
|
6447
|
-
return c.json({ error: "invalid_config", detail:
|
|
7559
|
+
} catch (err2) {
|
|
7560
|
+
return c.json({ error: "invalid_config", detail: err2.message }, 400);
|
|
6448
7561
|
}
|
|
6449
7562
|
});
|
|
6450
7563
|
}
|
|
@@ -6471,14 +7584,14 @@ function mountMetaApi(app, opts) {
|
|
|
6471
7584
|
|
|
6472
7585
|
// src/console/api/runs.ts
|
|
6473
7586
|
import { streamSSE } from "hono/streaming";
|
|
6474
|
-
import { z as
|
|
6475
|
-
import
|
|
6476
|
-
import
|
|
7587
|
+
import { z as z8 } from "zod";
|
|
7588
|
+
import path32 from "path";
|
|
7589
|
+
import fs39 from "fs";
|
|
6477
7590
|
var active = /* @__PURE__ */ new Map();
|
|
6478
7591
|
var EVENT_BUF_CAP = 512;
|
|
6479
|
-
var StartBody =
|
|
6480
|
-
feature:
|
|
6481
|
-
storyId:
|
|
7592
|
+
var StartBody = z8.object({
|
|
7593
|
+
feature: z8.string().min(1),
|
|
7594
|
+
storyId: z8.string().min(1)
|
|
6482
7595
|
});
|
|
6483
7596
|
function emptyStats(durationMs) {
|
|
6484
7597
|
return {
|
|
@@ -6534,11 +7647,36 @@ function mountRunsApi(app, opts) {
|
|
|
6534
7647
|
const done = (async () => {
|
|
6535
7648
|
const cfgFresh = loadConfig(opts.paths.configFile);
|
|
6536
7649
|
const planner = cfgFresh.planner;
|
|
6537
|
-
const
|
|
6538
|
-
const model =
|
|
7650
|
+
const modelId = modelFor(planner.provider, "plan", planner.modelOverride);
|
|
7651
|
+
const { model } = resolveModel(planner.provider, modelId, apiKey);
|
|
6539
7652
|
const budget = new Budget(planner.budget);
|
|
6540
7653
|
const cacheEnabled = planner.cache?.enabled ?? true;
|
|
6541
|
-
const
|
|
7654
|
+
const scoutEnabled = planner.stages?.scout?.enabled !== false;
|
|
7655
|
+
const validationEnabled = planner.validation?.enabled !== false;
|
|
7656
|
+
const strictValidation = planner.validation?.strict === true;
|
|
7657
|
+
const maxScoutFiles = planner.stages?.scout?.maxFiles ?? 12;
|
|
7658
|
+
let scoutModelId = "";
|
|
7659
|
+
let scoutModelHandle;
|
|
7660
|
+
let scoutSystemPrompt;
|
|
7661
|
+
if (scoutEnabled) {
|
|
7662
|
+
scoutModelId = modelFor(
|
|
7663
|
+
planner.provider,
|
|
7664
|
+
"scout",
|
|
7665
|
+
planner.modelOverride,
|
|
7666
|
+
planner.stages?.scout?.modelOverride
|
|
7667
|
+
);
|
|
7668
|
+
scoutModelHandle = resolveModel(planner.provider, scoutModelId, apiKey).model;
|
|
7669
|
+
}
|
|
7670
|
+
const repoMap = buildRepoMap(opts.paths.root, { format: "tree" });
|
|
7671
|
+
if (scoutEnabled) {
|
|
7672
|
+
scoutSystemPrompt = composeScoutSystemPrompt({
|
|
7673
|
+
projectRoots: cfgFresh.project.projectRoots ?? ["."],
|
|
7674
|
+
primaryLanguage: cfgFresh.project.primaryLanguage ?? "",
|
|
7675
|
+
trackerType: cfgFresh.tracker.type,
|
|
7676
|
+
repoMap,
|
|
7677
|
+
plansIndex: buildPlansIndex(opts.paths)
|
|
7678
|
+
});
|
|
7679
|
+
}
|
|
6542
7680
|
const systemPrompt = composeSystemPrompt({
|
|
6543
7681
|
projectRoots: cfgFresh.project.projectRoots ?? ["."],
|
|
6544
7682
|
primaryLanguage: cfgFresh.project.primaryLanguage ?? "",
|
|
@@ -6546,15 +7684,15 @@ function mountRunsApi(app, opts) {
|
|
|
6546
7684
|
repoMap
|
|
6547
7685
|
});
|
|
6548
7686
|
const userPrompt = composeUserPrompt({
|
|
6549
|
-
intakeContent:
|
|
7687
|
+
intakeContent: fs39.readFileSync(story.intakePath, "utf8")
|
|
6550
7688
|
});
|
|
6551
7689
|
let result;
|
|
6552
7690
|
try {
|
|
6553
7691
|
result = await runPlanner({
|
|
6554
7692
|
root: opts.paths.root,
|
|
6555
|
-
provider,
|
|
6556
7693
|
model,
|
|
6557
|
-
|
|
7694
|
+
provider: planner.provider,
|
|
7695
|
+
modelId,
|
|
6558
7696
|
systemPrompt,
|
|
6559
7697
|
userPrompt,
|
|
6560
7698
|
budget,
|
|
@@ -6562,11 +7700,27 @@ function mountRunsApi(app, opts) {
|
|
|
6562
7700
|
cacheEnabled,
|
|
6563
7701
|
events: bus,
|
|
6564
7702
|
runId,
|
|
6565
|
-
abort: controller.signal
|
|
7703
|
+
abort: controller.signal,
|
|
7704
|
+
stages: {
|
|
7705
|
+
scout: {
|
|
7706
|
+
enabled: scoutEnabled,
|
|
7707
|
+
model: scoutModelHandle,
|
|
7708
|
+
modelId: scoutModelId,
|
|
7709
|
+
maxFiles: maxScoutFiles,
|
|
7710
|
+
maxOutputTokens: planner.stages?.scout?.maxOutputTokens ?? 2048
|
|
7711
|
+
}
|
|
7712
|
+
},
|
|
7713
|
+
validation: { enabled: validationEnabled, strict: strictValidation },
|
|
7714
|
+
toolsEnabled: planner.tools,
|
|
7715
|
+
scoutSystemPrompt
|
|
6566
7716
|
});
|
|
6567
|
-
} catch (
|
|
7717
|
+
} catch (err2) {
|
|
6568
7718
|
const durationMs = Date.now() - startedAt;
|
|
6569
|
-
|
|
7719
|
+
const last = eventBuffer[eventBuffer.length - 1];
|
|
7720
|
+
const isRateLimitAbort = last?.kind === "rate_limit" && last.phase === "aborted";
|
|
7721
|
+
if (!isRateLimitAbort) {
|
|
7722
|
+
bus.emit({ kind: "error", runId, message: err2.message });
|
|
7723
|
+
}
|
|
6570
7724
|
bus.emit({
|
|
6571
7725
|
kind: "done",
|
|
6572
7726
|
runId,
|
|
@@ -6578,20 +7732,25 @@ function mountRunsApi(app, opts) {
|
|
|
6578
7732
|
});
|
|
6579
7733
|
return;
|
|
6580
7734
|
}
|
|
6581
|
-
const
|
|
7735
|
+
const issueCount = result.validation?.issues.length ?? 0;
|
|
7736
|
+
const issuesByKind = summariseIssuesByKind(result.validation?.issues ?? []);
|
|
7737
|
+
const validationBlocks = strictValidation && validationEnabled && issueCount > 0;
|
|
7738
|
+
const runSuccess = result.finishedNormally && !result.timedOut && !result.userCancelled && !validationBlocks;
|
|
6582
7739
|
let planFile = null;
|
|
6583
7740
|
if (result.planText.trim()) {
|
|
6584
7741
|
const snap = budget.snapshot();
|
|
6585
7742
|
const header = buildMetadataHeader({
|
|
6586
7743
|
provider: planner.provider,
|
|
6587
|
-
model,
|
|
7744
|
+
model: modelId,
|
|
6588
7745
|
reads: snap.reads,
|
|
6589
7746
|
bytes: snap.bytes,
|
|
6590
7747
|
inputTokens: snap.usage.inputTokens,
|
|
6591
7748
|
outputTokens: snap.usage.outputTokens,
|
|
6592
7749
|
durationMs: Date.now() - startedAt,
|
|
6593
|
-
|
|
6594
|
-
|
|
7750
|
+
planStatus: runSuccess ? void 0 : "partial",
|
|
7751
|
+
scoutEnabled,
|
|
7752
|
+
validationEnabled,
|
|
7753
|
+
validationIssueCount: issueCount
|
|
6595
7754
|
});
|
|
6596
7755
|
const out = writePlanFile({
|
|
6597
7756
|
paths: opts.paths,
|
|
@@ -6601,19 +7760,19 @@ function mountRunsApi(app, opts) {
|
|
|
6601
7760
|
metadataHeader: header,
|
|
6602
7761
|
partial: !runSuccess
|
|
6603
7762
|
});
|
|
6604
|
-
planFile =
|
|
7763
|
+
planFile = path32.relative(opts.paths.root, out.planFile);
|
|
6605
7764
|
}
|
|
6606
7765
|
try {
|
|
6607
7766
|
await writeLastRun(opts.paths, {
|
|
6608
7767
|
stats: result.stats,
|
|
6609
7768
|
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
6610
7769
|
provider: planner.provider,
|
|
6611
|
-
model
|
|
7770
|
+
model: modelId
|
|
6612
7771
|
});
|
|
6613
7772
|
await appendRun(opts.paths, {
|
|
6614
7773
|
runId,
|
|
6615
7774
|
provider: planner.provider,
|
|
6616
|
-
model,
|
|
7775
|
+
model: modelId,
|
|
6617
7776
|
feature: story.feature,
|
|
6618
7777
|
storyId: story.id,
|
|
6619
7778
|
startedAt: new Date(startedAt).toISOString(),
|
|
@@ -6623,7 +7782,19 @@ function mountRunsApi(app, opts) {
|
|
|
6623
7782
|
planFile,
|
|
6624
7783
|
stats: result.stats,
|
|
6625
7784
|
cacheEnabled,
|
|
6626
|
-
durationMs: Date.now() - startedAt
|
|
7785
|
+
durationMs: Date.now() - startedAt,
|
|
7786
|
+
scout: {
|
|
7787
|
+
enabled: scoutEnabled,
|
|
7788
|
+
selectedCount: result.scout?.selected.length,
|
|
7789
|
+
tokensUsed: result.scout?.tokensUsed,
|
|
7790
|
+
durationMs: result.scout?.durationMs
|
|
7791
|
+
},
|
|
7792
|
+
validation: {
|
|
7793
|
+
enabled: validationEnabled,
|
|
7794
|
+
issuesCount: issueCount,
|
|
7795
|
+
issuesByKind,
|
|
7796
|
+
durationMs: result.validation?.durationMs
|
|
7797
|
+
}
|
|
6627
7798
|
});
|
|
6628
7799
|
} catch {
|
|
6629
7800
|
}
|
|
@@ -6696,7 +7867,7 @@ function mountRunsApi(app, opts) {
|
|
|
6696
7867
|
}
|
|
6697
7868
|
|
|
6698
7869
|
// src/console/api/secrets.ts
|
|
6699
|
-
import { z as
|
|
7870
|
+
import { z as z9 } from "zod";
|
|
6700
7871
|
|
|
6701
7872
|
// src/core/mask-token.ts
|
|
6702
7873
|
function maskToken(t) {
|
|
@@ -6734,26 +7905,26 @@ function mountSecretsApi(app, opts) {
|
|
|
6734
7905
|
}
|
|
6735
7906
|
});
|
|
6736
7907
|
});
|
|
6737
|
-
const PutBody =
|
|
6738
|
-
planner:
|
|
6739
|
-
anthropic:
|
|
6740
|
-
openai:
|
|
6741
|
-
google:
|
|
7908
|
+
const PutBody = z9.object({
|
|
7909
|
+
planner: z9.object({
|
|
7910
|
+
anthropic: z9.string().optional(),
|
|
7911
|
+
openai: z9.string().optional(),
|
|
7912
|
+
google: z9.string().optional()
|
|
6742
7913
|
}).optional(),
|
|
6743
|
-
tracker:
|
|
6744
|
-
jira:
|
|
6745
|
-
host:
|
|
6746
|
-
email:
|
|
6747
|
-
token:
|
|
7914
|
+
tracker: z9.object({
|
|
7915
|
+
jira: z9.object({
|
|
7916
|
+
host: z9.string().optional(),
|
|
7917
|
+
email: z9.string().optional(),
|
|
7918
|
+
token: z9.string().optional()
|
|
6748
7919
|
}).optional(),
|
|
6749
|
-
azure:
|
|
6750
|
-
organization:
|
|
6751
|
-
project:
|
|
6752
|
-
pat:
|
|
7920
|
+
azure: z9.object({
|
|
7921
|
+
organization: z9.string().optional(),
|
|
7922
|
+
project: z9.string().optional(),
|
|
7923
|
+
pat: z9.string().optional()
|
|
6753
7924
|
}).optional(),
|
|
6754
|
-
github:
|
|
6755
|
-
host:
|
|
6756
|
-
pat:
|
|
7925
|
+
github: z9.object({
|
|
7926
|
+
host: z9.string().optional(),
|
|
7927
|
+
pat: z9.string().optional()
|
|
6757
7928
|
}).optional()
|
|
6758
7929
|
}).optional()
|
|
6759
7930
|
});
|
|
@@ -6793,12 +7964,12 @@ function mountSecretsApi(app, opts) {
|
|
|
6793
7964
|
}
|
|
6794
7965
|
|
|
6795
7966
|
// src/console/api/tracker.ts
|
|
6796
|
-
import { z as
|
|
6797
|
-
var SearchQuery =
|
|
6798
|
-
var ImportBody =
|
|
6799
|
-
issueId:
|
|
6800
|
-
feature:
|
|
6801
|
-
withAttachments:
|
|
7967
|
+
import { z as z10 } from "zod";
|
|
7968
|
+
var SearchQuery = z10.object({ q: z10.string().max(500) });
|
|
7969
|
+
var ImportBody = z10.object({
|
|
7970
|
+
issueId: z10.string().min(1),
|
|
7971
|
+
feature: z10.string().min(1).max(80),
|
|
7972
|
+
withAttachments: z10.boolean().optional()
|
|
6802
7973
|
});
|
|
6803
7974
|
function mountTrackerApi(app, opts) {
|
|
6804
7975
|
app.get("/api/tracker/search", async (c) => {
|
|
@@ -6806,18 +7977,18 @@ function mountTrackerApi(app, opts) {
|
|
|
6806
7977
|
if (!q.success) return c.json({ error: "invalid_query" }, 400);
|
|
6807
7978
|
const cfg = loadConfig(opts.paths.configFile);
|
|
6808
7979
|
const secrets = overlayTrackerEnv(loadSecrets(opts.paths.secretsFile));
|
|
6809
|
-
const { client, error
|
|
7980
|
+
const { client, error } = clientFor(cfg, secrets);
|
|
6810
7981
|
if (!client) {
|
|
6811
|
-
return c.json({ error:
|
|
7982
|
+
return c.json({ error: error?.kind ?? "unsupported", detail: error?.detail ?? "" }, 400);
|
|
6812
7983
|
}
|
|
6813
7984
|
try {
|
|
6814
7985
|
const results = await client.searchIssues(q.data.q);
|
|
6815
7986
|
return c.json({ ok: true, results });
|
|
6816
|
-
} catch (
|
|
6817
|
-
if (
|
|
6818
|
-
return c.json({ ok: false, kind:
|
|
7987
|
+
} catch (err2) {
|
|
7988
|
+
if (err2 instanceof TrackerError) {
|
|
7989
|
+
return c.json({ ok: false, kind: err2.kind, status: err2.statusCode, detail: err2.message }, 200);
|
|
6819
7990
|
}
|
|
6820
|
-
return c.json({ ok: false, detail:
|
|
7991
|
+
return c.json({ ok: false, detail: err2.message }, 200);
|
|
6821
7992
|
}
|
|
6822
7993
|
});
|
|
6823
7994
|
app.post("/api/tracker/import", async (c) => {
|
|
@@ -6825,9 +7996,9 @@ function mountTrackerApi(app, opts) {
|
|
|
6825
7996
|
if (!body.success) return c.json({ error: "invalid_body", issues: body.error.issues }, 400);
|
|
6826
7997
|
const cfg = loadConfig(opts.paths.configFile);
|
|
6827
7998
|
const secrets = overlayTrackerEnv(loadSecrets(opts.paths.secretsFile));
|
|
6828
|
-
const { client, error
|
|
7999
|
+
const { client, error } = clientFor(cfg, secrets);
|
|
6829
8000
|
if (!client) {
|
|
6830
|
-
return c.json({ error:
|
|
8001
|
+
return c.json({ error: error?.kind ?? "unsupported", detail: error?.detail ?? "" }, 400);
|
|
6831
8002
|
}
|
|
6832
8003
|
try {
|
|
6833
8004
|
const issue = await client.fetchIssue(body.data.issueId);
|
|
@@ -6840,9 +8011,9 @@ function mountTrackerApi(app, opts) {
|
|
|
6840
8011
|
config: cfg
|
|
6841
8012
|
});
|
|
6842
8013
|
return c.json(created, 201);
|
|
6843
|
-
} catch (
|
|
6844
|
-
if (
|
|
6845
|
-
return c.json({ error: "fetch_failed", detail:
|
|
8014
|
+
} catch (err2) {
|
|
8015
|
+
if (err2 instanceof TrackerError) return c.json({ error: err2.kind, detail: err2.message }, 502);
|
|
8016
|
+
return c.json({ error: "fetch_failed", detail: err2.message }, 500);
|
|
6846
8017
|
}
|
|
6847
8018
|
});
|
|
6848
8019
|
}
|
|
@@ -6939,17 +8110,17 @@ function countStories(stories) {
|
|
|
6939
8110
|
}
|
|
6940
8111
|
|
|
6941
8112
|
// src/console/api/projects.ts
|
|
6942
|
-
import
|
|
6943
|
-
import
|
|
8113
|
+
import fs40 from "fs/promises";
|
|
8114
|
+
import path33 from "path";
|
|
6944
8115
|
import os from "os";
|
|
6945
8116
|
var MAX = 10;
|
|
6946
8117
|
function recentFilePath() {
|
|
6947
|
-
return
|
|
8118
|
+
return path33.join(os.homedir(), ".squad", "recent-projects.json");
|
|
6948
8119
|
}
|
|
6949
8120
|
async function loadRecent() {
|
|
6950
8121
|
const RECENT_FILE = recentFilePath();
|
|
6951
8122
|
try {
|
|
6952
|
-
const raw = await
|
|
8123
|
+
const raw = await fs40.readFile(RECENT_FILE, "utf8");
|
|
6953
8124
|
const parsed = JSON.parse(raw);
|
|
6954
8125
|
return Array.isArray(parsed) ? parsed.slice(0, MAX) : [];
|
|
6955
8126
|
} catch {
|
|
@@ -6958,8 +8129,8 @@ async function loadRecent() {
|
|
|
6958
8129
|
}
|
|
6959
8130
|
async function saveRecent(list) {
|
|
6960
8131
|
const RECENT_FILE = recentFilePath();
|
|
6961
|
-
await
|
|
6962
|
-
await
|
|
8132
|
+
await fs40.mkdir(path33.dirname(RECENT_FILE), { recursive: true });
|
|
8133
|
+
await fs40.writeFile(RECENT_FILE, JSON.stringify(list.slice(0, MAX), null, 2) + "\n", "utf8");
|
|
6963
8134
|
}
|
|
6964
8135
|
function mountProjectsApi(app, opts) {
|
|
6965
8136
|
app.get("/api/projects/recent", async (c) => c.json(await loadRecent()));
|
|
@@ -6988,8 +8159,8 @@ function mountApi(app, opts) {
|
|
|
6988
8159
|
}
|
|
6989
8160
|
|
|
6990
8161
|
// src/console/static-assets.ts
|
|
6991
|
-
import
|
|
6992
|
-
import
|
|
8162
|
+
import fs41 from "fs";
|
|
8163
|
+
import path34 from "path";
|
|
6993
8164
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6994
8165
|
import { Readable } from "stream";
|
|
6995
8166
|
function pathnameForStatic(c) {
|
|
@@ -7019,18 +8190,18 @@ function mountStaticAssets(app, _opts) {
|
|
|
7019
8190
|
}
|
|
7020
8191
|
if (c.req.path.startsWith("/api/") || c.req.path.startsWith("/healthz")) return c.notFound();
|
|
7021
8192
|
if (!distDir) return c.text("UI not built. Run `pnpm -C console-ui build`.", 404);
|
|
7022
|
-
const indexHtml =
|
|
7023
|
-
if (!
|
|
7024
|
-
return c.html(
|
|
8193
|
+
const indexHtml = path34.join(distDir, "index.html");
|
|
8194
|
+
if (!fs41.existsSync(indexHtml)) return c.text("UI not built", 404);
|
|
8195
|
+
return c.html(fs41.readFileSync(indexHtml, "utf8"), 200, { "cache-control": "no-store" });
|
|
7025
8196
|
});
|
|
7026
8197
|
}
|
|
7027
8198
|
function assetResponse(c, distDir, pathname) {
|
|
7028
8199
|
if (!distDir) return c.text("UI not built", 404);
|
|
7029
8200
|
const rel = pathname.replace(/^\//, "");
|
|
7030
|
-
const abs =
|
|
8201
|
+
const abs = path34.normalize(path34.join(distDir, rel));
|
|
7031
8202
|
if (!isInside2(abs, distDir)) return c.text("forbidden", 403);
|
|
7032
|
-
if (!
|
|
7033
|
-
const body = Readable.toWeb(
|
|
8203
|
+
if (!fs41.existsSync(abs) || !fs41.statSync(abs).isFile()) return c.text("not found", 404);
|
|
8204
|
+
const body = Readable.toWeb(fs41.createReadStream(abs));
|
|
7034
8205
|
return new Response(body, {
|
|
7035
8206
|
headers: {
|
|
7036
8207
|
"content-type": contentTypeFor(abs),
|
|
@@ -7040,9 +8211,9 @@ function assetResponse(c, distDir, pathname) {
|
|
|
7040
8211
|
}
|
|
7041
8212
|
function serveFile(c, distDir, name, ct) {
|
|
7042
8213
|
if (!distDir) return c.text("UI not built", 404);
|
|
7043
|
-
const abs =
|
|
7044
|
-
if (!
|
|
7045
|
-
return new Response(
|
|
8214
|
+
const abs = path34.join(distDir, name);
|
|
8215
|
+
if (!fs41.existsSync(abs)) return c.text("not found", 404);
|
|
8216
|
+
return new Response(fs41.readFileSync(abs), {
|
|
7046
8217
|
headers: { "content-type": ct, "cache-control": "public, max-age=86400" }
|
|
7047
8218
|
});
|
|
7048
8219
|
}
|
|
@@ -7057,13 +8228,13 @@ function contentTypeFor(p) {
|
|
|
7057
8228
|
return "application/octet-stream";
|
|
7058
8229
|
}
|
|
7059
8230
|
function isInside2(abs, parent) {
|
|
7060
|
-
const a =
|
|
7061
|
-
const p =
|
|
8231
|
+
const a = path34.resolve(abs) + path34.sep;
|
|
8232
|
+
const p = path34.resolve(parent) + path34.sep;
|
|
7062
8233
|
return a.startsWith(p);
|
|
7063
8234
|
}
|
|
7064
8235
|
function resolveConsoleUiDist() {
|
|
7065
8236
|
const fromEnv = process.env.SQUAD_CONSOLE_UI_DIST;
|
|
7066
|
-
if (fromEnv &&
|
|
8237
|
+
if (fromEnv && fs41.existsSync(path34.join(fromEnv, "index.html"))) return fromEnv;
|
|
7067
8238
|
const candidates = [
|
|
7068
8239
|
fileURLToPath2(new URL("./console-ui/", import.meta.url)),
|
|
7069
8240
|
// dist/cli.js → dist/console-ui/
|
|
@@ -7075,7 +8246,7 @@ function resolveConsoleUiDist() {
|
|
|
7075
8246
|
// src/console/*.ts → project/console-ui/dist
|
|
7076
8247
|
];
|
|
7077
8248
|
for (const c of candidates) {
|
|
7078
|
-
if (
|
|
8249
|
+
if (fs41.existsSync(path34.join(c, "index.html"))) return c;
|
|
7079
8250
|
}
|
|
7080
8251
|
return null;
|
|
7081
8252
|
}
|
|
@@ -7151,13 +8322,13 @@ async function startConsoleServer(opts) {
|
|
|
7151
8322
|
return bindPort;
|
|
7152
8323
|
})();
|
|
7153
8324
|
const shutdown = (onClose) => {
|
|
7154
|
-
nodeServer.close((
|
|
7155
|
-
onClose(
|
|
8325
|
+
nodeServer.close((err2) => {
|
|
8326
|
+
onClose(err2);
|
|
7156
8327
|
});
|
|
7157
8328
|
};
|
|
7158
8329
|
const stop = () => {
|
|
7159
|
-
shutdown((
|
|
7160
|
-
if (
|
|
8330
|
+
shutdown((err2) => {
|
|
8331
|
+
if (err2) warning(`server close: ${err2.message}`);
|
|
7161
8332
|
resolveDone();
|
|
7162
8333
|
});
|
|
7163
8334
|
};
|
|
@@ -7167,9 +8338,9 @@ async function startConsoleServer(opts) {
|
|
|
7167
8338
|
port: actualPort,
|
|
7168
8339
|
done,
|
|
7169
8340
|
close: () => new Promise((resolve2, reject) => {
|
|
7170
|
-
shutdown((
|
|
7171
|
-
if (
|
|
7172
|
-
reject(
|
|
8341
|
+
shutdown((err2) => {
|
|
8342
|
+
if (err2) {
|
|
8343
|
+
reject(err2);
|
|
7173
8344
|
return;
|
|
7174
8345
|
}
|
|
7175
8346
|
resolve2();
|
|
@@ -7257,18 +8428,18 @@ async function runConsole(opts) {
|
|
|
7257
8428
|
}
|
|
7258
8429
|
|
|
7259
8430
|
// src/commands/rm/story.ts
|
|
7260
|
-
import
|
|
7261
|
-
import
|
|
8431
|
+
import fs43 from "fs";
|
|
8432
|
+
import path36 from "path";
|
|
7262
8433
|
import { confirm as confirm10 } from "@inquirer/prompts";
|
|
7263
8434
|
|
|
7264
8435
|
// src/commands/rm/feature.ts
|
|
7265
|
-
import
|
|
7266
|
-
import
|
|
8436
|
+
import fs42 from "fs";
|
|
8437
|
+
import path35 from "path";
|
|
7267
8438
|
import { confirm as confirm9 } from "@inquirer/prompts";
|
|
7268
8439
|
function countStoryDirs(storiesDir, feature) {
|
|
7269
|
-
const d =
|
|
7270
|
-
if (!
|
|
7271
|
-
return
|
|
8440
|
+
const d = path35.join(storiesDir, feature);
|
|
8441
|
+
if (!fs42.existsSync(d)) return 0;
|
|
8442
|
+
return fs42.readdirSync(d, { withFileTypes: true }).filter((x) => x.isDirectory()).length;
|
|
7272
8443
|
}
|
|
7273
8444
|
async function runRmFeature(featureSlug, opts = {}) {
|
|
7274
8445
|
const root = requireSquadRoot();
|
|
@@ -7302,17 +8473,17 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
7302
8473
|
feature = await pickFeatureInteractive(paths);
|
|
7303
8474
|
}
|
|
7304
8475
|
}
|
|
7305
|
-
const storyDir =
|
|
7306
|
-
const planDir =
|
|
8476
|
+
const storyDir = path35.join(paths.storiesDir, feature);
|
|
8477
|
+
const planDir = path35.join(paths.plansDir, feature);
|
|
7307
8478
|
const nStories = countStoryDirs(paths.storiesDir, feature);
|
|
7308
8479
|
const nPlans = listAllPlanListEntries(paths, feature).length;
|
|
7309
|
-
const hasOverview =
|
|
8480
|
+
const hasOverview = fs42.existsSync(path35.join(planDir, "00-overview.md")) ? 1 : 0;
|
|
7310
8481
|
const targets = [];
|
|
7311
|
-
if (
|
|
7312
|
-
targets.push({ label:
|
|
8482
|
+
if (fs42.existsSync(storyDir)) {
|
|
8483
|
+
targets.push({ label: path35.relative(root, storyDir), absPath: storyDir, kind: "dir" });
|
|
7313
8484
|
}
|
|
7314
|
-
if (
|
|
7315
|
-
targets.push({ label:
|
|
8485
|
+
if (fs42.existsSync(planDir)) {
|
|
8486
|
+
targets.push({ label: path35.relative(root, planDir), absPath: planDir, kind: "dir" });
|
|
7316
8487
|
}
|
|
7317
8488
|
divider("squad rm feature");
|
|
7318
8489
|
info(`Feature: ${feature}`);
|
|
@@ -7327,7 +8498,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
7327
8498
|
kv(t.kind, t.label, 6);
|
|
7328
8499
|
}
|
|
7329
8500
|
if (useTrash) {
|
|
7330
|
-
info(`Items will move to ${
|
|
8501
|
+
info(`Items will move to ${path35.relative(process.cwd(), paths.trashDir)}/<timestamp>/`);
|
|
7331
8502
|
} else if (!dryRun) {
|
|
7332
8503
|
warning("This is a permanent delete. Pass --trash to move to .squad/.trash/ instead.");
|
|
7333
8504
|
}
|
|
@@ -7360,7 +8531,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
7360
8531
|
useTrash
|
|
7361
8532
|
);
|
|
7362
8533
|
if (result.trashed) {
|
|
7363
|
-
success(`Feature removed. Items moved to ${
|
|
8534
|
+
success(`Feature removed. Items moved to ${path35.relative(root, result.trashed)}.`);
|
|
7364
8535
|
} else {
|
|
7365
8536
|
success("Feature removed (stories and plans directories).");
|
|
7366
8537
|
}
|
|
@@ -7401,11 +8572,11 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
7401
8572
|
}
|
|
7402
8573
|
}
|
|
7403
8574
|
const targets = [
|
|
7404
|
-
{ label:
|
|
8575
|
+
{ label: path36.relative(root, story.storyDir), absPath: story.storyDir, kind: "dir" }
|
|
7405
8576
|
];
|
|
7406
8577
|
if (story.planFile) {
|
|
7407
|
-
const planAbs =
|
|
7408
|
-
targets.push({ label:
|
|
8578
|
+
const planAbs = path36.join(paths.plansDir, story.feature, story.planFile);
|
|
8579
|
+
targets.push({ label: path36.relative(root, planAbs), absPath: planAbs, kind: "file" });
|
|
7409
8580
|
}
|
|
7410
8581
|
renderPreview("story", targets, {
|
|
7411
8582
|
dryRun,
|
|
@@ -7430,14 +8601,14 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
7430
8601
|
}
|
|
7431
8602
|
const result = deleteStoryRecord({ paths, story, trash: useTrash });
|
|
7432
8603
|
if (result.trashed) {
|
|
7433
|
-
success(`Removed (moved to ${
|
|
8604
|
+
success(`Removed (moved to ${path36.relative(root, result.trashed)}).`);
|
|
7434
8605
|
} else {
|
|
7435
8606
|
success("Removed story folder, plan file (if any), and overview row.");
|
|
7436
8607
|
}
|
|
7437
|
-
const featurePlanDir =
|
|
7438
|
-
const overviewPath =
|
|
7439
|
-
if (
|
|
7440
|
-
const body =
|
|
8608
|
+
const featurePlanDir = path36.join(paths.plansDir, story.feature);
|
|
8609
|
+
const overviewPath = path36.join(featurePlanDir, "00-overview.md");
|
|
8610
|
+
if (fs43.existsSync(overviewPath)) {
|
|
8611
|
+
const body = fs43.readFileSync(overviewPath, "utf8");
|
|
7441
8612
|
const onlyShell = countOverviewDataRows(body) === 0;
|
|
7442
8613
|
if (onlyShell && featureStoriesDirEmpty(paths, story.feature)) {
|
|
7443
8614
|
if (canPrompt) {
|
|
@@ -7457,7 +8628,7 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
7457
8628
|
}
|
|
7458
8629
|
|
|
7459
8630
|
// src/commands/rm/plan.ts
|
|
7460
|
-
import
|
|
8631
|
+
import path37 from "path";
|
|
7461
8632
|
import { confirm as confirm11 } from "@inquirer/prompts";
|
|
7462
8633
|
async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
7463
8634
|
const root = requireSquadRoot();
|
|
@@ -7493,7 +8664,7 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
7493
8664
|
}
|
|
7494
8665
|
const storyRows = listStories(paths, { feature: plan.feature });
|
|
7495
8666
|
const linked = storyRows.find((s) => s.planFile === plan.file);
|
|
7496
|
-
const targets = [{ label:
|
|
8667
|
+
const targets = [{ label: path37.relative(root, plan.abs), absPath: plan.abs, kind: "file" }];
|
|
7497
8668
|
renderPreview("plan", targets, {
|
|
7498
8669
|
dryRun,
|
|
7499
8670
|
trash: useTrash,
|
|
@@ -7516,8 +8687,8 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
7516
8687
|
}
|
|
7517
8688
|
}
|
|
7518
8689
|
trashOrDelete([plan.abs], paths.trashDir, useTrash);
|
|
7519
|
-
removeOverviewRowForPlanFile(
|
|
7520
|
-
const intakeHint = linked ?
|
|
8690
|
+
removeOverviewRowForPlanFile(path37.join(paths.plansDir, plan.feature), plan.file);
|
|
8691
|
+
const intakeHint = linked ? path37.relative(root, linked.intakePath) : `under .squad/stories/${plan.feature}/`;
|
|
7521
8692
|
success(`Plan removed. Run \`squad new-plan ${intakeHint}\` to regenerate.`);
|
|
7522
8693
|
}
|
|
7523
8694
|
|
|
@@ -7551,7 +8722,11 @@ program.command("new-story [feature-slug]").description("Scaffold a new story in
|
|
|
7551
8722
|
"--no-tracker",
|
|
7552
8723
|
"Skip tracker fetch AND drop the tracker-id requirement for this story"
|
|
7553
8724
|
).action(wrapArgs(runNewStory));
|
|
7554
|
-
program.command("new-plan [intake-path]").description("Generate a plan (via direct API when configured, otherwise copy-paste)").option("--no-clipboard", "Do not copy the composed prompt to clipboard (copy-paste mode only)").option("--feature <slug>", "Filter picker by feature slug").option("--all", "Include already-planned intakes in the picker", false).option("-y, --yes", "Skip confirmation prompts", false).option("--api", "Force direct planner API (fails if not configured)", false).option("--copy", "Force copy-paste mode even when planner API is configured", false).
|
|
8725
|
+
program.command("new-plan [intake-path]").description("Generate a plan (via direct API when configured, otherwise copy-paste)").option("--no-clipboard", "Do not copy the composed prompt to clipboard (copy-paste mode only)").option("--feature <slug>", "Filter picker by feature slug").option("--all", "Include already-planned intakes in the picker", false).option("-y, --yes", "Skip confirmation prompts", false).option("--api", "Force direct planner API (fails if not configured)", false).option("--copy", "Force copy-paste mode even when planner API is configured", false).option("--no-scout", "Disable the scout stage (draft only)", false).option("--scout-model <id>", "Override scout model id (cheap tier default from PLANNER_MODEL_MAP)").option(
|
|
8726
|
+
"--max-scout-files <n>",
|
|
8727
|
+
"Maximum files the scout can pre-select (default from config or 12)",
|
|
8728
|
+
(v) => parseInt(v, 10)
|
|
8729
|
+
).option("--no-validation", "Disable post-write validation", false).option("--strict-validation", "Write partial plan when validation finds issues", false).action(wrapArgs(runNewPlan));
|
|
7555
8730
|
program.command("status").description("Show squad-kit workspace status").action(wrap(runStatus));
|
|
7556
8731
|
program.command("doctor").description("Run a health check on the local .squad/ workspace and its external integrations").option("--fix", "Apply non-destructive repairs (gitignore, permissions, missing dirs)", false).option("--json", "Emit results as JSON to stdout (for scripting)", false).action(wrap(runDoctor));
|
|
7557
8732
|
program.command("migrate").description("Apply one-shot structural migrations to bring .squad/ to the latest version").option("--dry-run", "Show what would change without touching the filesystem", false).option("-y, --yes", "Skip the confirmation prompt", false).action(wrap(runMigrate));
|
|
@@ -7581,24 +8756,24 @@ config.command("remove-credential <section>").description('Delete credentials fo
|
|
|
7581
8756
|
(section, opts) => runConfigRemoveCredential(section, { yes: opts.yes })
|
|
7582
8757
|
)
|
|
7583
8758
|
);
|
|
7584
|
-
program.parseAsync(process.argv).catch((
|
|
7585
|
-
if (isSquadExit(
|
|
7586
|
-
if (
|
|
7587
|
-
process.exit(
|
|
8759
|
+
program.parseAsync(process.argv).catch((err2) => {
|
|
8760
|
+
if (isSquadExit(err2)) {
|
|
8761
|
+
if (err2.message.trim()) info(err2.message);
|
|
8762
|
+
process.exit(err2.exitCode);
|
|
7588
8763
|
}
|
|
7589
|
-
renderError(
|
|
8764
|
+
renderError(err2);
|
|
7590
8765
|
process.exit(1);
|
|
7591
8766
|
});
|
|
7592
8767
|
function wrap(fn) {
|
|
7593
8768
|
return async (opts) => {
|
|
7594
8769
|
try {
|
|
7595
8770
|
await fn(opts);
|
|
7596
|
-
} catch (
|
|
7597
|
-
if (isSquadExit(
|
|
7598
|
-
if (
|
|
7599
|
-
process.exit(
|
|
8771
|
+
} catch (err2) {
|
|
8772
|
+
if (isSquadExit(err2)) {
|
|
8773
|
+
if (err2.message.trim()) info(err2.message);
|
|
8774
|
+
process.exit(err2.exitCode);
|
|
7600
8775
|
}
|
|
7601
|
-
renderError(
|
|
8776
|
+
renderError(err2);
|
|
7602
8777
|
process.exit(1);
|
|
7603
8778
|
}
|
|
7604
8779
|
};
|
|
@@ -7607,12 +8782,12 @@ function wrapArgs(fn) {
|
|
|
7607
8782
|
return async (...args) => {
|
|
7608
8783
|
try {
|
|
7609
8784
|
await fn(...args);
|
|
7610
|
-
} catch (
|
|
7611
|
-
if (isSquadExit(
|
|
7612
|
-
if (
|
|
7613
|
-
process.exit(
|
|
8785
|
+
} catch (err2) {
|
|
8786
|
+
if (isSquadExit(err2)) {
|
|
8787
|
+
if (err2.message.trim()) info(err2.message);
|
|
8788
|
+
process.exit(err2.exitCode);
|
|
7614
8789
|
}
|
|
7615
|
-
renderError(
|
|
8790
|
+
renderError(err2);
|
|
7616
8791
|
process.exit(1);
|
|
7617
8792
|
}
|
|
7618
8793
|
};
|