squad-kit 0.2.2 → 0.3.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 +69 -1
- package/README.md +21 -7
- package/dist/cli.js +832 -360
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -172,7 +172,7 @@ function renderError(err) {
|
|
|
172
172
|
// src/commands/init.ts
|
|
173
173
|
import fs14 from "fs";
|
|
174
174
|
import path6 from "path";
|
|
175
|
-
import { input as
|
|
175
|
+
import { input as input2, select as select3, checkbox, confirm as confirm5, password as password3 } from "@inquirer/prompts";
|
|
176
176
|
|
|
177
177
|
// src/core/paths.ts
|
|
178
178
|
import path from "path";
|
|
@@ -212,8 +212,8 @@ function requireSquadRoot(startDir = process.cwd()) {
|
|
|
212
212
|
}
|
|
213
213
|
return root;
|
|
214
214
|
}
|
|
215
|
-
function slugify(
|
|
216
|
-
return
|
|
215
|
+
function slugify(input5) {
|
|
216
|
+
return input5.toLowerCase().trim().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
// src/core/gitignore.ts
|
|
@@ -222,42 +222,38 @@ import path2 from "path";
|
|
|
222
222
|
var SQUAD_SECRETS_PATTERN = ".squad/secrets.yaml";
|
|
223
223
|
var SQUAD_ATTACHMENTS_PATTERN = ".squad/stories/**/attachments/";
|
|
224
224
|
var SQUAD_TRASH_PATTERN = ".squad/.trash/";
|
|
225
|
+
var SQUAD_LAST_RUN_PATTERN = ".squad/.last-run.json";
|
|
226
|
+
var SQUAD_LAST_COPY_PROMPT_PATTERN = ".squad/.last-copy-prompt.md";
|
|
225
227
|
var MANAGED_BLOCK_HEADER = "# Managed by squad-kit \u2014 do not edit this block";
|
|
226
228
|
var MANAGED_BLOCK_FOOTER = "# End squad-kit block";
|
|
227
229
|
function ensureGitignore(repoRoot) {
|
|
228
230
|
const file = path2.join(repoRoot, ".gitignore");
|
|
229
231
|
const text = fs2.existsSync(file) ? fs2.readFileSync(file, "utf8") : "";
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
232
|
+
const requiredPatterns = [
|
|
233
|
+
SQUAD_SECRETS_PATTERN,
|
|
234
|
+
SQUAD_ATTACHMENTS_PATTERN,
|
|
235
|
+
SQUAD_TRASH_PATTERN,
|
|
236
|
+
SQUAD_LAST_RUN_PATTERN,
|
|
237
|
+
SQUAD_LAST_COPY_PROMPT_PATTERN
|
|
238
|
+
];
|
|
239
|
+
const allPresent = requiredPatterns.every((p) => text.includes(p));
|
|
240
|
+
if (allPresent) return false;
|
|
241
|
+
if (!text.includes(SQUAD_SECRETS_PATTERN)) {
|
|
242
|
+
const block = ["", MANAGED_BLOCK_HEADER, ...requiredPatterns, MANAGED_BLOCK_FOOTER, ""].join("\n");
|
|
243
|
+
const next2 = text.length === 0 ? block.trimStart() : text.endsWith("\n") ? text + block : text + "\n" + block;
|
|
244
|
+
fs2.writeFileSync(file, next2, "utf8");
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
let next = text;
|
|
248
|
+
for (const pattern of requiredPatterns) {
|
|
249
|
+
if (next.includes(pattern)) continue;
|
|
250
|
+
if (next.includes(MANAGED_BLOCK_FOOTER)) {
|
|
251
|
+
next = next.replace(MANAGED_BLOCK_FOOTER, pattern + "\n" + MANAGED_BLOCK_FOOTER);
|
|
242
252
|
} else {
|
|
243
|
-
next = next.replace(/\n?$/, (m) => m +
|
|
244
|
-
}
|
|
245
|
-
if (next !== text) {
|
|
246
|
-
fs2.writeFileSync(file, next, "utf8");
|
|
247
|
-
return true;
|
|
253
|
+
next = next.replace(/\n?$/, (m) => m + pattern + "\n");
|
|
248
254
|
}
|
|
249
255
|
}
|
|
250
|
-
if (
|
|
251
|
-
const block = [
|
|
252
|
-
"",
|
|
253
|
-
MANAGED_BLOCK_HEADER,
|
|
254
|
-
SQUAD_SECRETS_PATTERN,
|
|
255
|
-
SQUAD_ATTACHMENTS_PATTERN,
|
|
256
|
-
SQUAD_TRASH_PATTERN,
|
|
257
|
-
MANAGED_BLOCK_FOOTER,
|
|
258
|
-
""
|
|
259
|
-
].join("\n");
|
|
260
|
-
const next = text.length === 0 ? block.trimStart() : text.endsWith("\n") ? text + block : text + "\n" + block;
|
|
256
|
+
if (next !== text) {
|
|
261
257
|
fs2.writeFileSync(file, next, "utf8");
|
|
262
258
|
return true;
|
|
263
259
|
}
|
|
@@ -280,15 +276,15 @@ var DEFAULT_CONFIG = {
|
|
|
280
276
|
planner: void 0
|
|
281
277
|
};
|
|
282
278
|
var FORBIDDEN_KEYS = ["apikey", "api_key", "token", "secret", "credential", "credentials"];
|
|
283
|
-
function rejectSecretsInYaml(node, configFile,
|
|
279
|
+
function rejectSecretsInYaml(node, configFile, path26 = []) {
|
|
284
280
|
if (node === null || typeof node !== "object") return;
|
|
285
281
|
for (const [key, value] of Object.entries(node)) {
|
|
286
282
|
if (FORBIDDEN_KEYS.includes(key.toLowerCase())) {
|
|
287
283
|
throw new Error(
|
|
288
|
-
`Refusing to load ${configFile}: key "${[...
|
|
284
|
+
`Refusing to load ${configFile}: key "${[...path26, 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.`
|
|
289
285
|
);
|
|
290
286
|
}
|
|
291
|
-
if (typeof value === "object") rejectSecretsInYaml(value, configFile, [...
|
|
287
|
+
if (typeof value === "object") rejectSecretsInYaml(value, configFile, [...path26, key]);
|
|
292
288
|
}
|
|
293
289
|
}
|
|
294
290
|
function mergePlanner(base, override) {
|
|
@@ -306,6 +302,9 @@ function mergePlanner(base, override) {
|
|
|
306
302
|
modelOverride: {
|
|
307
303
|
...base?.modelOverride ?? {},
|
|
308
304
|
...override.modelOverride ?? {}
|
|
305
|
+
},
|
|
306
|
+
cache: {
|
|
307
|
+
enabled: override.cache?.enabled ?? base?.cache?.enabled ?? true
|
|
309
308
|
}
|
|
310
309
|
};
|
|
311
310
|
if (merged.modelOverride) {
|
|
@@ -350,6 +349,20 @@ function loadConfig(configFile) {
|
|
|
350
349
|
rejectSecretsInYaml(parsed, configFile);
|
|
351
350
|
const merged = mergeConfig(DEFAULT_CONFIG, parsed);
|
|
352
351
|
validateModelOverride(merged.planner?.modelOverride, configFile);
|
|
352
|
+
if (merged.planner?.cache !== void 0) {
|
|
353
|
+
const c = merged.planner.cache;
|
|
354
|
+
if (typeof c !== "object" || c === null || Array.isArray(c)) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`planner.cache in ${configFile} must be an object with \`enabled: true\` or \`false\`. Run \`squad config set planner\` to set prompt caching.`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
const ce = c.enabled;
|
|
360
|
+
if (typeof ce !== "boolean") {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`planner.cache.enabled in ${configFile} must be a boolean (not a quoted string). Run \`squad config set planner\` to set prompt caching, or use \`true\` or \`false\` unquoted in YAML.`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
353
366
|
if (merged.planner?.enabled) {
|
|
354
367
|
if (!["anthropic", "openai", "google"].includes(merged.planner.provider)) {
|
|
355
368
|
throw new Error(
|
|
@@ -696,7 +709,7 @@ async function runConfigShow(opts) {
|
|
|
696
709
|
|
|
697
710
|
// src/commands/config/set-planner.ts
|
|
698
711
|
import fs9 from "fs";
|
|
699
|
-
import { confirm as confirm2,
|
|
712
|
+
import { confirm as confirm2, password as password2, select } from "@inquirer/prompts";
|
|
700
713
|
|
|
701
714
|
// src/tracker/env-overlay.ts
|
|
702
715
|
function overlayTrackerEnv(base) {
|
|
@@ -842,7 +855,8 @@ function newPlannerBlock(provider) {
|
|
|
842
855
|
maxFileReads: 25,
|
|
843
856
|
maxContextBytes: 5e4,
|
|
844
857
|
maxDurationSeconds: 180
|
|
845
|
-
}
|
|
858
|
+
},
|
|
859
|
+
cache: { enabled: true }
|
|
846
860
|
};
|
|
847
861
|
}
|
|
848
862
|
function mergePlannerKeyIntoSecrets(base, provider, key) {
|
|
@@ -993,22 +1007,11 @@ async function runConfigSetPlanner(opts = {}) {
|
|
|
993
1007
|
success("Planner key saved to .squad/secrets.yaml");
|
|
994
1008
|
info(".squad/secrets.yaml updated (chmod 0600 on POSIX)");
|
|
995
1009
|
}
|
|
996
|
-
const
|
|
997
|
-
message: "
|
|
998
|
-
default:
|
|
1010
|
+
const cacheEnabled = await confirm2({
|
|
1011
|
+
message: "Enable prompt caching? (Recommended \u2014 reduces billed tokens by ~70% on most providers.)",
|
|
1012
|
+
default: nextPlanner.cache?.enabled ?? true
|
|
999
1013
|
});
|
|
1000
|
-
|
|
1001
|
-
const modelId = await input2({
|
|
1002
|
-
message: "Plan model id (leave empty to use the default for this provider):",
|
|
1003
|
-
default: ""
|
|
1004
|
-
});
|
|
1005
|
-
if (modelId.trim().length > 0 && nextPlanner) {
|
|
1006
|
-
nextPlanner = {
|
|
1007
|
-
...nextPlanner,
|
|
1008
|
-
modelOverride: { ...nextPlanner.modelOverride ?? {}, [provider]: modelId.trim() }
|
|
1009
|
-
};
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1014
|
+
nextPlanner = { ...nextPlanner, cache: { enabled: cacheEnabled } };
|
|
1012
1015
|
}
|
|
1013
1016
|
const next = { ...config2, planner: nextPlanner };
|
|
1014
1017
|
saveConfig(paths.configFile, next);
|
|
@@ -1029,6 +1032,7 @@ async function runConfigSetPlanner(opts = {}) {
|
|
|
1029
1032
|
kv("credential", `${cred.source} (${sourceText})`, 10);
|
|
1030
1033
|
}
|
|
1031
1034
|
if (process.env.CI === "true" || !cred) {
|
|
1035
|
+
printPlannerNextSteps(Boolean(cred));
|
|
1032
1036
|
return;
|
|
1033
1037
|
}
|
|
1034
1038
|
const listed = await fetchProviderModelIds(provider, cred.value);
|
|
@@ -1042,6 +1046,23 @@ async function runConfigSetPlanner(opts = {}) {
|
|
|
1042
1046
|
} else {
|
|
1043
1047
|
info("Credential check: models API responded OK for this key.");
|
|
1044
1048
|
}
|
|
1049
|
+
printPlannerNextSteps(true);
|
|
1050
|
+
}
|
|
1051
|
+
function printPlannerNextSteps(credentialReady) {
|
|
1052
|
+
blank();
|
|
1053
|
+
step("Next:");
|
|
1054
|
+
if (!credentialReady) {
|
|
1055
|
+
info("1) Save a planner key: re-run `squad config set planner` and paste the key when prompted.");
|
|
1056
|
+
info("2) Verify with `squad doctor` \u2014 all planner checks should turn green.");
|
|
1057
|
+
info("3) Then run `squad new-story <slug>` and `squad new-plan --api` to generate your first plan.");
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
info("1) Verify with `squad doctor` \u2014 every planner check should be green.");
|
|
1061
|
+
info("2) Create a story: squad new-story <feature-slug> (or --no-tracker for a manual story).");
|
|
1062
|
+
info("3) Fill the generated intake.md, then run `squad new-plan --api` to generate the plan.");
|
|
1063
|
+
info(
|
|
1064
|
+
"4) To change provider, key, or disable caching later: re-run `squad config set planner`. Model overrides are edited directly in .squad/config.yaml (planner.modelOverride)."
|
|
1065
|
+
);
|
|
1045
1066
|
}
|
|
1046
1067
|
|
|
1047
1068
|
// src/commands/config/set-tracker.ts
|
|
@@ -1526,6 +1547,7 @@ async function runConfigSetTracker(opts = {}) {
|
|
|
1526
1547
|
kv("project", next.tracker.project, 10);
|
|
1527
1548
|
}
|
|
1528
1549
|
if (process.env.CI === "true") {
|
|
1550
|
+
printTrackerNextSteps(next.tracker.type);
|
|
1529
1551
|
return;
|
|
1530
1552
|
}
|
|
1531
1553
|
const reloaded = loadConfig(paths.configFile);
|
|
@@ -1549,6 +1571,20 @@ async function runConfigSetTracker(opts = {}) {
|
|
|
1549
1571
|
);
|
|
1550
1572
|
}
|
|
1551
1573
|
}
|
|
1574
|
+
printTrackerNextSteps(next.tracker.type);
|
|
1575
|
+
}
|
|
1576
|
+
function printTrackerNextSteps(type) {
|
|
1577
|
+
blank();
|
|
1578
|
+
step("Next:");
|
|
1579
|
+
if (type === "none") {
|
|
1580
|
+
info("1) Tracker is off. Create stories manually with `squad new-story <slug> --no-tracker`.");
|
|
1581
|
+
info("2) Re-enable any time with `squad config set tracker`.");
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
info("1) Verify with `squad doctor` \u2014 tracker checks should be green.");
|
|
1585
|
+
info(`2) Create a story: squad new-story <feature-slug> --id <${type === "jira" ? "JIRA-123" : "azure-work-item-id"}>`);
|
|
1586
|
+
info(" squad-kit auto-fetches the title, description, and attachments into the intake.");
|
|
1587
|
+
info("3) Review the generated intake.md, then run `squad new-plan --api` to generate the plan.");
|
|
1552
1588
|
}
|
|
1553
1589
|
|
|
1554
1590
|
// src/commands/config/unset-tracker.ts
|
|
@@ -1671,8 +1707,8 @@ async function runInit(opts) {
|
|
|
1671
1707
|
agents: parseAgentsFlag(opts.agents)
|
|
1672
1708
|
};
|
|
1673
1709
|
const answers = opts.yes ? defaults : {
|
|
1674
|
-
name: await
|
|
1675
|
-
language: await
|
|
1710
|
+
name: await input2({ message: "Project name", default: defaults.name }),
|
|
1711
|
+
language: await input2({ message: "Primary language", default: defaults.language }),
|
|
1676
1712
|
tracker: await select3({
|
|
1677
1713
|
message: "Issue tracker",
|
|
1678
1714
|
choices: [
|
|
@@ -1893,7 +1929,7 @@ Plan meta-prompts (\`generate-plan.md\`, \`story-skeleton.md\`) ship inside the
|
|
|
1893
1929
|
// src/commands/new-story.ts
|
|
1894
1930
|
import fs16 from "fs";
|
|
1895
1931
|
import path8 from "path";
|
|
1896
|
-
import { input as
|
|
1932
|
+
import { input as input3, select as select4 } from "@inquirer/prompts";
|
|
1897
1933
|
|
|
1898
1934
|
// src/core/stories.ts
|
|
1899
1935
|
import fs15 from "fs";
|
|
@@ -2024,7 +2060,7 @@ async function runNewStory(featureSlug, opts) {
|
|
|
2024
2060
|
}
|
|
2025
2061
|
}
|
|
2026
2062
|
if (!featureSlugInput) {
|
|
2027
|
-
featureSlugInput = (await
|
|
2063
|
+
featureSlugInput = (await input3({
|
|
2028
2064
|
message: "New feature slug (kebab-case):",
|
|
2029
2065
|
validate: validateFeatureSlugForPrompt
|
|
2030
2066
|
})).trim();
|
|
@@ -2063,7 +2099,7 @@ async function runNewStory(featureSlug, opts) {
|
|
|
2063
2099
|
noTracker = true;
|
|
2064
2100
|
trackerNeedsId = false;
|
|
2065
2101
|
} else {
|
|
2066
|
-
id = (await
|
|
2102
|
+
id = (await input3({
|
|
2067
2103
|
message: `${config2.tracker.type} work-item id:`,
|
|
2068
2104
|
validate: (v) => validateTrackerId(config2.tracker.type, v.trim()) ? true : `Invalid ${config2.tracker.type} id format.`
|
|
2069
2105
|
})).trim();
|
|
@@ -2082,12 +2118,12 @@ async function runNewStory(featureSlug, opts) {
|
|
|
2082
2118
|
);
|
|
2083
2119
|
}
|
|
2084
2120
|
} else {
|
|
2085
|
-
title = (await
|
|
2121
|
+
title = (await input3({ message: "Short story title:" })).trim();
|
|
2086
2122
|
if (!title) throw new Error("Title cannot be empty. Run `squad new-story` again and enter a short title, or add `--yes` to fail fast in scripts.");
|
|
2087
2123
|
}
|
|
2088
2124
|
}
|
|
2089
2125
|
if (interactive && !title) {
|
|
2090
|
-
const maybe = (await
|
|
2126
|
+
const maybe = (await input3({ message: "Short title (optional, press Enter to skip):", default: "" })).trim();
|
|
2091
2127
|
if (maybe) title = maybe;
|
|
2092
2128
|
}
|
|
2093
2129
|
const storyFolderName = id ? trackerIdForFilename(config2.tracker.type, id) : slugify(title ?? "");
|
|
@@ -2279,29 +2315,96 @@ function ensureFeatureOverview(featurePlanDir, slug) {
|
|
|
2279
2315
|
}
|
|
2280
2316
|
|
|
2281
2317
|
// src/commands/new-plan.ts
|
|
2282
|
-
import
|
|
2283
|
-
import
|
|
2318
|
+
import fs22 from "fs";
|
|
2319
|
+
import path15 from "path";
|
|
2284
2320
|
import { confirm as confirm6, select as select5 } from "@inquirer/prompts";
|
|
2285
2321
|
|
|
2286
2322
|
// src/utils/clipboard.ts
|
|
2287
|
-
import { spawn } from "child_process";
|
|
2323
|
+
import { spawn, spawnSync } from "child_process";
|
|
2288
2324
|
async function copyToClipboard(text) {
|
|
2289
|
-
const
|
|
2290
|
-
if (
|
|
2325
|
+
const candidates = clipboardCandidates();
|
|
2326
|
+
if (candidates.length === 0) {
|
|
2327
|
+
return { ok: false, reason: noCandidateReason() };
|
|
2328
|
+
}
|
|
2329
|
+
let lastReason;
|
|
2330
|
+
for (const cmd of candidates) {
|
|
2331
|
+
const reason = cmd.unavailableReason?.();
|
|
2332
|
+
if (reason) {
|
|
2333
|
+
lastReason = reason;
|
|
2334
|
+
continue;
|
|
2335
|
+
}
|
|
2336
|
+
const res = await tryCopy(cmd, text);
|
|
2337
|
+
if (res.ok) return res;
|
|
2338
|
+
lastReason = res.reason;
|
|
2339
|
+
}
|
|
2340
|
+
return { ok: false, reason: lastReason ?? "no working clipboard tool found" };
|
|
2341
|
+
}
|
|
2342
|
+
function tryCopy(cmd, text) {
|
|
2291
2343
|
return new Promise((resolve) => {
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2344
|
+
let child;
|
|
2345
|
+
try {
|
|
2346
|
+
child = spawn(cmd.command, cmd.args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
2347
|
+
} catch (err) {
|
|
2348
|
+
resolve({ ok: false, reason: `${cmd.command} failed to spawn: ${err.message}` });
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
let stderr = "";
|
|
2352
|
+
child.stderr?.on("data", (chunk) => {
|
|
2353
|
+
stderr += chunk.toString();
|
|
2354
|
+
});
|
|
2355
|
+
child.on("error", (err) => {
|
|
2356
|
+
resolve({ ok: false, reason: `${cmd.command} spawn error: ${err.message}` });
|
|
2357
|
+
});
|
|
2358
|
+
child.on("close", (code) => {
|
|
2359
|
+
if (code === 0) {
|
|
2360
|
+
resolve({ ok: true, tool: cmd.command });
|
|
2361
|
+
} else {
|
|
2362
|
+
resolve({
|
|
2363
|
+
ok: false,
|
|
2364
|
+
reason: `${cmd.command} exited with code ${code}${stderr ? `: ${stderr.trim().slice(0, 200)}` : ""}`
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2367
|
+
});
|
|
2368
|
+
child.stdin?.write(text);
|
|
2369
|
+
child.stdin?.end();
|
|
2297
2370
|
});
|
|
2298
2371
|
}
|
|
2299
|
-
function
|
|
2300
|
-
if (process.platform === "darwin")
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
if (process.
|
|
2304
|
-
|
|
2372
|
+
function clipboardCandidates() {
|
|
2373
|
+
if (process.platform === "darwin") {
|
|
2374
|
+
return [{ command: "pbcopy", args: [] }];
|
|
2375
|
+
}
|
|
2376
|
+
if (process.platform === "win32") {
|
|
2377
|
+
return [{ command: "clip", args: [] }];
|
|
2378
|
+
}
|
|
2379
|
+
const out = [];
|
|
2380
|
+
if (process.env.WAYLAND_DISPLAY) {
|
|
2381
|
+
out.push({ command: "wl-copy", args: [], unavailableReason: hasBinary("wl-copy") });
|
|
2382
|
+
}
|
|
2383
|
+
if (process.env.DISPLAY) {
|
|
2384
|
+
out.push({ command: "xclip", args: ["-selection", "clipboard"], unavailableReason: hasBinary("xclip") });
|
|
2385
|
+
out.push({ command: "xsel", args: ["--clipboard", "--input"], unavailableReason: hasBinary("xsel") });
|
|
2386
|
+
}
|
|
2387
|
+
if (process.env.TERMUX_VERSION) {
|
|
2388
|
+
out.push({ command: "termux-clipboard-set", args: [], unavailableReason: hasBinary("termux-clipboard-set") });
|
|
2389
|
+
}
|
|
2390
|
+
return out;
|
|
2391
|
+
}
|
|
2392
|
+
function hasBinary(bin) {
|
|
2393
|
+
return () => {
|
|
2394
|
+
const which = process.platform === "win32" ? "where" : "which";
|
|
2395
|
+
const res = spawnSync(which, [bin], { stdio: "ignore" });
|
|
2396
|
+
if (res.status === 0) return void 0;
|
|
2397
|
+
return `${bin} not found on PATH`;
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
function noCandidateReason() {
|
|
2401
|
+
if (process.platform === "linux") {
|
|
2402
|
+
if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY && !process.env.TERMUX_VERSION) {
|
|
2403
|
+
return "no display server detected (headless / SSH) \u2014 install xclip or xsel, or use the saved file";
|
|
2404
|
+
}
|
|
2405
|
+
return "no clipboard tool detected \u2014 install xclip, xsel, or wl-copy";
|
|
2406
|
+
}
|
|
2407
|
+
return `no clipboard tool detected on ${process.platform}`;
|
|
2305
2408
|
}
|
|
2306
2409
|
|
|
2307
2410
|
// src/planner/loop.ts
|
|
@@ -2323,8 +2426,8 @@ var READ_FILE_TOOL = {
|
|
|
2323
2426
|
}
|
|
2324
2427
|
};
|
|
2325
2428
|
var MAX_BYTES_PER_READ = 32e3;
|
|
2326
|
-
function readFileTool(root, budget,
|
|
2327
|
-
const raw =
|
|
2429
|
+
function readFileTool(root, budget, input5) {
|
|
2430
|
+
const raw = input5?.path;
|
|
2328
2431
|
if (typeof raw !== "string" || raw.length === 0) {
|
|
2329
2432
|
return error('read_file: missing or invalid "path" argument.');
|
|
2330
2433
|
}
|
|
@@ -2455,117 +2558,16 @@ function rateLimitMessage(err) {
|
|
|
2455
2558
|
].join("\n");
|
|
2456
2559
|
}
|
|
2457
2560
|
|
|
2458
|
-
// src/planner/
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
const
|
|
2463
|
-
const
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
let budgetExhausted = false;
|
|
2467
|
-
let finishedNormally = false;
|
|
2468
|
-
while (iterations < maxIter) {
|
|
2469
|
-
iterations += 1;
|
|
2470
|
-
if (input6.budget.timedOut()) {
|
|
2471
|
-
return { planText: accumulatedText, budgetExhausted, timedOut: true, finishedNormally: false, iterations };
|
|
2472
|
-
}
|
|
2473
|
-
if (input6.budget.overCost()) {
|
|
2474
|
-
return { planText: accumulatedText, budgetExhausted: true, timedOut: false, finishedNormally: false, iterations };
|
|
2475
|
-
}
|
|
2476
|
-
const request = {
|
|
2477
|
-
systemPrompt: input6.systemPrompt,
|
|
2478
|
-
model: input6.model,
|
|
2479
|
-
tools: [READ_FILE_TOOL],
|
|
2480
|
-
turns,
|
|
2481
|
-
apiKey: input6.apiKey,
|
|
2482
|
-
maxOutputTokens: 8192
|
|
2483
|
-
};
|
|
2484
|
-
let response = await input6.provider.send(request);
|
|
2485
|
-
if (response.usage) {
|
|
2486
|
-
input6.budget.recordUsage(response.usage);
|
|
2487
|
-
input6.onUsage?.(response.usage);
|
|
2488
|
-
}
|
|
2489
|
-
let retriedRateLimit = false;
|
|
2490
|
-
let retrySkippedReason;
|
|
2491
|
-
if (response.stopReason === "error" && response.errorKind === "rate_limit") {
|
|
2492
|
-
const asked = response.retryAfterSec;
|
|
2493
|
-
if (asked !== void 0 && asked > MAX_RATE_LIMIT_RETRY_SEC) {
|
|
2494
|
-
retrySkippedReason = "retry_after_too_long";
|
|
2495
|
-
} else {
|
|
2496
|
-
const waitSec = Math.min(asked ?? 10, MAX_RATE_LIMIT_RETRY_SEC);
|
|
2497
|
-
input6.onRateLimit?.(waitSec);
|
|
2498
|
-
await (input6.sleep ?? defaultSleep)(waitSec * 1e3);
|
|
2499
|
-
response = await input6.provider.send(request);
|
|
2500
|
-
if (response.usage) {
|
|
2501
|
-
input6.budget.recordUsage(response.usage);
|
|
2502
|
-
input6.onUsage?.(response.usage);
|
|
2503
|
-
}
|
|
2504
|
-
retriedRateLimit = true;
|
|
2505
|
-
}
|
|
2506
|
-
}
|
|
2507
|
-
if (response.stopReason === "error") {
|
|
2508
|
-
throw composePlannerError(input6.provider.name, response, retriedRateLimit, retrySkippedReason);
|
|
2509
|
-
}
|
|
2510
|
-
if (response.text) {
|
|
2511
|
-
accumulatedText += response.text;
|
|
2512
|
-
input6.onAssistantText?.(response.text);
|
|
2513
|
-
}
|
|
2514
|
-
if (response.stopReason === "max_tokens" && !response.toolCalls?.length) {
|
|
2515
|
-
return { planText: accumulatedText, budgetExhausted, timedOut: false, finishedNormally: false, iterations };
|
|
2516
|
-
}
|
|
2517
|
-
if (response.stopReason === "end_turn" || !response.toolCalls?.length) {
|
|
2518
|
-
finishedNormally = true;
|
|
2519
|
-
return { planText: accumulatedText, budgetExhausted, timedOut: false, finishedNormally, iterations };
|
|
2520
|
-
}
|
|
2521
|
-
turns.push({
|
|
2522
|
-
role: "assistant",
|
|
2523
|
-
text: response.text,
|
|
2524
|
-
toolCalls: response.toolCalls
|
|
2525
|
-
});
|
|
2526
|
-
const toolResults = [];
|
|
2527
|
-
for (const tc of response.toolCalls) {
|
|
2528
|
-
if (tc.name !== READ_FILE_TOOL.name) {
|
|
2529
|
-
toolResults.push({ toolCallId: tc.id, content: `unknown tool "${tc.name}"`, isError: true });
|
|
2530
|
-
continue;
|
|
2531
|
-
}
|
|
2532
|
-
const before = input6.budget.snapshot().bytes;
|
|
2533
|
-
const result = readFileTool(input6.root, input6.budget, tc.input);
|
|
2534
|
-
const after = input6.budget.snapshot().bytes;
|
|
2535
|
-
input6.onToolCall?.(tc, after - before, after);
|
|
2536
|
-
if (result.isError && (/budget/i.test(result.content) || /max file reads/i.test(result.content))) {
|
|
2537
|
-
budgetExhausted = true;
|
|
2538
|
-
}
|
|
2539
|
-
toolResults.push({ toolCallId: tc.id, content: result.content, isError: result.isError });
|
|
2540
|
-
}
|
|
2541
|
-
turns.push({ role: "user", toolResults });
|
|
2542
|
-
if (budgetExhausted) {
|
|
2543
|
-
turns.push({
|
|
2544
|
-
role: "user",
|
|
2545
|
-
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."
|
|
2546
|
-
});
|
|
2547
|
-
}
|
|
2548
|
-
}
|
|
2549
|
-
return { planText: accumulatedText, budgetExhausted, timedOut: false, finishedNormally: false, iterations };
|
|
2550
|
-
}
|
|
2551
|
-
function composePlannerError(providerName, response, retriedRateLimit, retrySkippedReason) {
|
|
2552
|
-
if (response.errorKind === "rate_limit") {
|
|
2553
|
-
return new Error(
|
|
2554
|
-
rateLimitMessage({
|
|
2555
|
-
provider: providerName,
|
|
2556
|
-
retryAfterSec: response.retryAfterSec,
|
|
2557
|
-
rawBody: response.rawError ?? "",
|
|
2558
|
-
retryAlreadyAttempted: retriedRateLimit,
|
|
2559
|
-
retrySkippedReason,
|
|
2560
|
-
maxRetrySec: MAX_RATE_LIMIT_RETRY_SEC
|
|
2561
|
-
})
|
|
2562
|
-
);
|
|
2563
|
-
}
|
|
2564
|
-
if (response.errorKind === "model_not_found") {
|
|
2565
|
-
return new Error(response.rawError ?? "planner: model not found");
|
|
2561
|
+
// src/planner/stable-json.ts
|
|
2562
|
+
function sortRecordKeys(v) {
|
|
2563
|
+
if (v === null || typeof v !== "object") return v;
|
|
2564
|
+
if (Array.isArray(v)) return v.map((x) => sortRecordKeys(x));
|
|
2565
|
+
const o = v;
|
|
2566
|
+
const out = {};
|
|
2567
|
+
for (const k of Object.keys(o).sort()) {
|
|
2568
|
+
out[k] = sortRecordKeys(o[k]);
|
|
2566
2569
|
}
|
|
2567
|
-
|
|
2568
|
-
return new Error(`${base} Run \`squad doctor\` to diagnose, or retry \u2014 most 5xx errors are transient.`);
|
|
2570
|
+
return out;
|
|
2569
2571
|
}
|
|
2570
2572
|
|
|
2571
2573
|
// src/planner/providers/anthropic.ts
|
|
@@ -2577,14 +2579,39 @@ var anthropicProvider = {
|
|
|
2577
2579
|
return callAnthropic(req);
|
|
2578
2580
|
}
|
|
2579
2581
|
};
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
+
function buildAnthropicBody(req) {
|
|
2583
|
+
if (req.cacheEnabled === false) {
|
|
2584
|
+
const base2 = {
|
|
2585
|
+
model: req.model,
|
|
2586
|
+
max_tokens: req.maxOutputTokens ?? 4096,
|
|
2587
|
+
system: req.systemPrompt,
|
|
2588
|
+
tools: req.tools.map(toAnthropicTool)
|
|
2589
|
+
};
|
|
2590
|
+
if (req.turns.length === 0) {
|
|
2591
|
+
return base2;
|
|
2592
|
+
}
|
|
2593
|
+
return { ...base2, messages: req.turns.map((t) => toAnthropicMessage(t)) };
|
|
2594
|
+
}
|
|
2595
|
+
const base = {
|
|
2582
2596
|
model: req.model,
|
|
2583
2597
|
max_tokens: req.maxOutputTokens ?? 4096,
|
|
2584
|
-
system:
|
|
2585
|
-
|
|
2586
|
-
|
|
2598
|
+
system: [
|
|
2599
|
+
{
|
|
2600
|
+
type: "text",
|
|
2601
|
+
text: req.systemPrompt,
|
|
2602
|
+
cache_control: { type: "ephemeral" }
|
|
2603
|
+
}
|
|
2604
|
+
],
|
|
2605
|
+
tools: req.tools.map(toAnthropicTool)
|
|
2587
2606
|
};
|
|
2607
|
+
if (req.turns.length === 0) {
|
|
2608
|
+
return base;
|
|
2609
|
+
}
|
|
2610
|
+
return { ...base, messages: toMessagesWithCacheMarkers(req.turns) };
|
|
2611
|
+
}
|
|
2612
|
+
async function callAnthropic(req) {
|
|
2613
|
+
const built = buildAnthropicBody(req);
|
|
2614
|
+
const body = { ...built, messages: built.messages ?? [] };
|
|
2588
2615
|
const res = await fetch(API_URL, {
|
|
2589
2616
|
method: "POST",
|
|
2590
2617
|
headers: {
|
|
@@ -2624,26 +2651,73 @@ async function callAnthropic(req) {
|
|
|
2624
2651
|
stopReason: mapStop(json.stop_reason),
|
|
2625
2652
|
usage: {
|
|
2626
2653
|
inputTokens: json.usage?.input_tokens ?? 0,
|
|
2627
|
-
outputTokens: json.usage?.output_tokens ?? 0
|
|
2654
|
+
outputTokens: json.usage?.output_tokens ?? 0,
|
|
2655
|
+
cacheCreationTokens: json.usage?.cache_creation_input_tokens ?? 0,
|
|
2656
|
+
cacheReadTokens: json.usage?.cache_read_input_tokens ?? 0
|
|
2628
2657
|
}
|
|
2629
2658
|
};
|
|
2630
2659
|
}
|
|
2660
|
+
function toMessagesWithCacheMarkers(turns) {
|
|
2661
|
+
let lastUserWithToolResultIdx = -1;
|
|
2662
|
+
for (let i = turns.length - 1; i >= 0; i -= 1) {
|
|
2663
|
+
const t = turns[i];
|
|
2664
|
+
if (t.role === "user" && hasToolResultBlock(t)) {
|
|
2665
|
+
lastUserWithToolResultIdx = i;
|
|
2666
|
+
break;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
return turns.map((turn, idx) => {
|
|
2670
|
+
const message = toAnthropicMessage(turn);
|
|
2671
|
+
if (idx === lastUserWithToolResultIdx) {
|
|
2672
|
+
return attachCacheControlToLastToolResult(message);
|
|
2673
|
+
}
|
|
2674
|
+
return message;
|
|
2675
|
+
});
|
|
2676
|
+
}
|
|
2677
|
+
function hasToolResultBlock(turn) {
|
|
2678
|
+
return (turn.toolResults?.length ?? 0) > 0;
|
|
2679
|
+
}
|
|
2680
|
+
function attachCacheControlToLastToolResult(msg) {
|
|
2681
|
+
const content = [...msg.content];
|
|
2682
|
+
for (let i = content.length - 1; i >= 0; i -= 1) {
|
|
2683
|
+
const block = content[i];
|
|
2684
|
+
if (block.type === "tool_result") {
|
|
2685
|
+
content[i] = { ...block, cache_control: { type: "ephemeral" } };
|
|
2686
|
+
break;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
return { ...msg, content };
|
|
2690
|
+
}
|
|
2631
2691
|
function toAnthropicTool(t) {
|
|
2632
|
-
return {
|
|
2692
|
+
return {
|
|
2693
|
+
name: t.name,
|
|
2694
|
+
description: t.description,
|
|
2695
|
+
input_schema: sortRecordKeys(t.inputSchema)
|
|
2696
|
+
};
|
|
2633
2697
|
}
|
|
2634
2698
|
function toAnthropicMessage(turn) {
|
|
2635
2699
|
const blocks = [];
|
|
2636
2700
|
if (turn.text) blocks.push({ type: "text", text: turn.text });
|
|
2637
|
-
if (turn.toolCalls)
|
|
2638
|
-
for (const tc of turn.toolCalls)
|
|
2639
|
-
|
|
2640
|
-
|
|
2701
|
+
if (turn.toolCalls) {
|
|
2702
|
+
for (const tc of turn.toolCalls) {
|
|
2703
|
+
blocks.push({
|
|
2704
|
+
type: "tool_use",
|
|
2705
|
+
id: tc.id,
|
|
2706
|
+
name: tc.name,
|
|
2707
|
+
input: sortRecordKeys(tc.input)
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
if (turn.toolResults) {
|
|
2712
|
+
for (const tr of turn.toolResults) {
|
|
2641
2713
|
blocks.push({
|
|
2642
2714
|
type: "tool_result",
|
|
2643
2715
|
tool_use_id: tr.toolCallId,
|
|
2644
2716
|
content: tr.content,
|
|
2645
2717
|
is_error: tr.isError ?? false
|
|
2646
2718
|
});
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2647
2721
|
return { role: turn.role === "assistant" ? "assistant" : "user", content: blocks };
|
|
2648
2722
|
}
|
|
2649
2723
|
function mapStop(r) {
|
|
@@ -2660,15 +2734,23 @@ var openaiProvider = {
|
|
|
2660
2734
|
return callOpenAI(req);
|
|
2661
2735
|
}
|
|
2662
2736
|
};
|
|
2663
|
-
|
|
2664
|
-
const
|
|
2665
|
-
for (const turn of req.turns) messages.push(...toOpenAIMessages(turn));
|
|
2666
|
-
const body = {
|
|
2737
|
+
function buildOpenAIBody(req) {
|
|
2738
|
+
const base = {
|
|
2667
2739
|
model: req.model,
|
|
2668
|
-
|
|
2669
|
-
tools: req.tools.map(toOpenAITool)
|
|
2670
|
-
max_completion_tokens: req.maxOutputTokens ?? 4096
|
|
2740
|
+
max_completion_tokens: req.maxOutputTokens ?? 4096,
|
|
2741
|
+
tools: req.tools.map(toOpenAITool)
|
|
2671
2742
|
};
|
|
2743
|
+
if (req.turns.length === 0) {
|
|
2744
|
+
return base;
|
|
2745
|
+
}
|
|
2746
|
+
const messages = [{ role: "system", content: req.systemPrompt }];
|
|
2747
|
+
for (const turn of req.turns) messages.push(...toOpenAIMessages(turn));
|
|
2748
|
+
return { ...base, messages };
|
|
2749
|
+
}
|
|
2750
|
+
async function callOpenAI(req) {
|
|
2751
|
+
const built = buildOpenAIBody(req);
|
|
2752
|
+
const messages = built.messages && built.messages.length > 0 ? built.messages : [{ role: "system", content: req.systemPrompt }];
|
|
2753
|
+
const body = { ...built, messages };
|
|
2672
2754
|
const res = await fetch(API_URL2, {
|
|
2673
2755
|
method: "POST",
|
|
2674
2756
|
headers: {
|
|
@@ -2718,7 +2800,14 @@ async function callOpenAI(req) {
|
|
|
2718
2800
|
};
|
|
2719
2801
|
}
|
|
2720
2802
|
function toOpenAITool(t) {
|
|
2721
|
-
return {
|
|
2803
|
+
return {
|
|
2804
|
+
type: "function",
|
|
2805
|
+
function: {
|
|
2806
|
+
name: t.name,
|
|
2807
|
+
description: t.description,
|
|
2808
|
+
parameters: sortRecordKeys(t.inputSchema)
|
|
2809
|
+
}
|
|
2810
|
+
};
|
|
2722
2811
|
}
|
|
2723
2812
|
function toOpenAIMessages(turn) {
|
|
2724
2813
|
const out = [];
|
|
@@ -2729,7 +2818,10 @@ function toOpenAIMessages(turn) {
|
|
|
2729
2818
|
tool_calls: turn.toolCalls?.map((tc) => ({
|
|
2730
2819
|
id: tc.id,
|
|
2731
2820
|
type: "function",
|
|
2732
|
-
function: {
|
|
2821
|
+
function: {
|
|
2822
|
+
name: tc.name,
|
|
2823
|
+
arguments: JSON.stringify(sortRecordKeys(tc.input))
|
|
2824
|
+
}
|
|
2733
2825
|
}))
|
|
2734
2826
|
});
|
|
2735
2827
|
} else {
|
|
@@ -2749,7 +2841,7 @@ function mapStop2(r) {
|
|
|
2749
2841
|
}
|
|
2750
2842
|
function safeJson(raw) {
|
|
2751
2843
|
try {
|
|
2752
|
-
return JSON.parse(raw);
|
|
2844
|
+
return sortRecordKeys(JSON.parse(raw));
|
|
2753
2845
|
} catch {
|
|
2754
2846
|
return {};
|
|
2755
2847
|
}
|
|
@@ -2763,14 +2855,23 @@ var googleProvider = {
|
|
|
2763
2855
|
return callGoogle(req);
|
|
2764
2856
|
}
|
|
2765
2857
|
};
|
|
2766
|
-
|
|
2767
|
-
const
|
|
2768
|
-
const body = {
|
|
2858
|
+
function buildGoogleBody(req) {
|
|
2859
|
+
const base = {
|
|
2769
2860
|
system_instruction: { parts: [{ text: req.systemPrompt }] },
|
|
2770
|
-
contents: req.turns.map(toGoogleContent),
|
|
2771
|
-
tools: req.tools.length ? [{ functionDeclarations: req.tools.map(toGoogleTool) }] : void 0,
|
|
2772
2861
|
generationConfig: { maxOutputTokens: req.maxOutputTokens ?? 4096 }
|
|
2773
2862
|
};
|
|
2863
|
+
if (req.tools.length) {
|
|
2864
|
+
base.tools = [{ functionDeclarations: req.tools.map(toGoogleTool) }];
|
|
2865
|
+
}
|
|
2866
|
+
if (req.turns.length === 0) {
|
|
2867
|
+
return base;
|
|
2868
|
+
}
|
|
2869
|
+
return { ...base, contents: req.turns.map(toGoogleContent) };
|
|
2870
|
+
}
|
|
2871
|
+
async function callGoogle(req) {
|
|
2872
|
+
const url = `${API_BASE}/${encodeURIComponent(req.model)}:generateContent?key=${encodeURIComponent(req.apiKey)}`;
|
|
2873
|
+
const built = buildGoogleBody(req);
|
|
2874
|
+
const body = { ...built, contents: built.contents ?? [] };
|
|
2774
2875
|
const res = await fetch(url, {
|
|
2775
2876
|
method: "POST",
|
|
2776
2877
|
headers: { "content-type": "application/json" },
|
|
@@ -2805,7 +2906,7 @@ async function callGoogle(req) {
|
|
|
2805
2906
|
const toolCalls = parts.filter((p) => "functionCall" in p && !!p.functionCall).map((p, i) => ({
|
|
2806
2907
|
id: `gcall_${i}_${p.functionCall.name}`,
|
|
2807
2908
|
name: p.functionCall.name,
|
|
2808
|
-
input: p.functionCall.args
|
|
2909
|
+
input: p.functionCall.args ? sortRecordKeys(p.functionCall.args) : {}
|
|
2809
2910
|
}));
|
|
2810
2911
|
return {
|
|
2811
2912
|
text: textOut || void 0,
|
|
@@ -2818,14 +2919,17 @@ async function callGoogle(req) {
|
|
|
2818
2919
|
};
|
|
2819
2920
|
}
|
|
2820
2921
|
function toGoogleTool(t) {
|
|
2821
|
-
return { name: t.name, description: t.description, parameters: t.inputSchema };
|
|
2922
|
+
return { name: t.name, description: t.description, parameters: sortRecordKeys(t.inputSchema) };
|
|
2822
2923
|
}
|
|
2823
2924
|
function toGoogleContent(turn) {
|
|
2824
2925
|
const role = turn.role === "assistant" ? "model" : "user";
|
|
2825
2926
|
const parts = [];
|
|
2826
2927
|
if (turn.text) parts.push({ text: turn.text });
|
|
2827
|
-
if (turn.toolCalls)
|
|
2828
|
-
for (const tc of turn.toolCalls)
|
|
2928
|
+
if (turn.toolCalls) {
|
|
2929
|
+
for (const tc of turn.toolCalls) {
|
|
2930
|
+
parts.push({ functionCall: { name: tc.name, args: sortRecordKeys(tc.input) } });
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2829
2933
|
if (turn.toolResults) {
|
|
2830
2934
|
for (const tr of turn.toolResults) {
|
|
2831
2935
|
parts.push({
|
|
@@ -2845,6 +2949,208 @@ function mapStop3(r) {
|
|
|
2845
2949
|
return "tool_use";
|
|
2846
2950
|
}
|
|
2847
2951
|
|
|
2952
|
+
// src/planner/providers/prefix.ts
|
|
2953
|
+
function prefixOf(provider, req) {
|
|
2954
|
+
const trimmed = { ...req, turns: req.turns.slice(0, -1) };
|
|
2955
|
+
const s = (() => {
|
|
2956
|
+
switch (provider) {
|
|
2957
|
+
case "anthropic":
|
|
2958
|
+
return JSON.stringify(buildAnthropicBody(trimmed));
|
|
2959
|
+
case "openai":
|
|
2960
|
+
return JSON.stringify(buildOpenAIBody(trimmed));
|
|
2961
|
+
case "google":
|
|
2962
|
+
return JSON.stringify(buildGoogleBody(trimmed));
|
|
2963
|
+
}
|
|
2964
|
+
})();
|
|
2965
|
+
return s.length > 0 ? s.slice(0, -1) : s;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
// src/planner/loop.ts
|
|
2969
|
+
var MAX_RATE_LIMIT_RETRY_SEC = 90;
|
|
2970
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2971
|
+
function buildPlannerRunStats(budget, turns, runStartedAt) {
|
|
2972
|
+
const u = budget.snapshot().usage;
|
|
2973
|
+
const cacheRead = u.cacheReadTokens ?? 0;
|
|
2974
|
+
const cacheCreate = u.cacheCreationTokens ?? 0;
|
|
2975
|
+
const inTok = u.inputTokens;
|
|
2976
|
+
const outTok = u.outputTokens;
|
|
2977
|
+
const totalInput = inTok + cacheRead;
|
|
2978
|
+
const cacheHitRatio = totalInput === 0 ? 0 : Math.round(cacheRead / totalInput * 100) / 100;
|
|
2979
|
+
return {
|
|
2980
|
+
turns,
|
|
2981
|
+
inputTokens: inTok,
|
|
2982
|
+
outputTokens: outTok,
|
|
2983
|
+
cacheCreationTokens: cacheCreate,
|
|
2984
|
+
cacheReadTokens: cacheRead,
|
|
2985
|
+
cacheHitRatio,
|
|
2986
|
+
durationMs: Date.now() - runStartedAt
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
async function runPlanner(input5) {
|
|
2990
|
+
const runStartedAt = Date.now();
|
|
2991
|
+
const maxIter = input5.maxIterations ?? 40;
|
|
2992
|
+
const turns = [{ role: "user", text: input5.userPrompt }];
|
|
2993
|
+
let accumulatedText = "";
|
|
2994
|
+
let iterations = 0;
|
|
2995
|
+
let budgetExhausted = false;
|
|
2996
|
+
let finishedNormally = false;
|
|
2997
|
+
let prevRequestPrefix;
|
|
2998
|
+
while (iterations < maxIter) {
|
|
2999
|
+
iterations += 1;
|
|
3000
|
+
if (input5.budget.timedOut()) {
|
|
3001
|
+
return {
|
|
3002
|
+
planText: accumulatedText,
|
|
3003
|
+
budgetExhausted,
|
|
3004
|
+
timedOut: true,
|
|
3005
|
+
finishedNormally: false,
|
|
3006
|
+
iterations,
|
|
3007
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
if (input5.budget.overCost()) {
|
|
3011
|
+
return {
|
|
3012
|
+
planText: accumulatedText,
|
|
3013
|
+
budgetExhausted: true,
|
|
3014
|
+
timedOut: false,
|
|
3015
|
+
finishedNormally: false,
|
|
3016
|
+
iterations,
|
|
3017
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
3018
|
+
};
|
|
3019
|
+
}
|
|
3020
|
+
const request = {
|
|
3021
|
+
systemPrompt: input5.systemPrompt,
|
|
3022
|
+
model: input5.model,
|
|
3023
|
+
tools: [READ_FILE_TOOL],
|
|
3024
|
+
turns,
|
|
3025
|
+
apiKey: input5.apiKey,
|
|
3026
|
+
maxOutputTokens: 8192,
|
|
3027
|
+
cacheEnabled: input5.cacheEnabled ?? true
|
|
3028
|
+
};
|
|
3029
|
+
if (process.env.NODE_ENV !== "production" && process.env.NODE_ENV !== "test" && prevRequestPrefix !== void 0) {
|
|
3030
|
+
const currentPrefix = prefixOf(input5.provider.name, request);
|
|
3031
|
+
if (!currentPrefix.startsWith(prevRequestPrefix)) {
|
|
3032
|
+
const byte = firstByteDiff(prevRequestPrefix, currentPrefix);
|
|
3033
|
+
warning(
|
|
3034
|
+
`planner: prefix mutation at turn ${iterations}, byte ${byte} \u2014 caching will not hit.`
|
|
3035
|
+
);
|
|
3036
|
+
}
|
|
3037
|
+
}
|
|
3038
|
+
let response = await input5.provider.send(request);
|
|
3039
|
+
if (response.usage) {
|
|
3040
|
+
input5.budget.recordUsage(response.usage);
|
|
3041
|
+
input5.onUsage?.(response.usage);
|
|
3042
|
+
}
|
|
3043
|
+
let retriedRateLimit = false;
|
|
3044
|
+
let retrySkippedReason;
|
|
3045
|
+
if (response.stopReason === "error" && response.errorKind === "rate_limit") {
|
|
3046
|
+
const asked = response.retryAfterSec;
|
|
3047
|
+
if (asked !== void 0 && asked > MAX_RATE_LIMIT_RETRY_SEC) {
|
|
3048
|
+
retrySkippedReason = "retry_after_too_long";
|
|
3049
|
+
} else {
|
|
3050
|
+
const waitSec = Math.min(asked ?? 10, MAX_RATE_LIMIT_RETRY_SEC);
|
|
3051
|
+
input5.onRateLimit?.(waitSec);
|
|
3052
|
+
await (input5.sleep ?? defaultSleep)(waitSec * 1e3);
|
|
3053
|
+
response = await input5.provider.send(request);
|
|
3054
|
+
if (response.usage) {
|
|
3055
|
+
input5.budget.recordUsage(response.usage);
|
|
3056
|
+
input5.onUsage?.(response.usage);
|
|
3057
|
+
}
|
|
3058
|
+
retriedRateLimit = true;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
if (response.stopReason === "error") {
|
|
3062
|
+
throw composePlannerError(input5.provider.name, response, retriedRateLimit, retrySkippedReason);
|
|
3063
|
+
}
|
|
3064
|
+
prevRequestPrefix = prefixOf(input5.provider.name, request);
|
|
3065
|
+
if (response.text) {
|
|
3066
|
+
accumulatedText += response.text;
|
|
3067
|
+
input5.onAssistantText?.(response.text);
|
|
3068
|
+
}
|
|
3069
|
+
if (response.stopReason === "max_tokens" && !response.toolCalls?.length) {
|
|
3070
|
+
return {
|
|
3071
|
+
planText: accumulatedText,
|
|
3072
|
+
budgetExhausted,
|
|
3073
|
+
timedOut: false,
|
|
3074
|
+
finishedNormally: false,
|
|
3075
|
+
iterations,
|
|
3076
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
3079
|
+
if (response.stopReason === "end_turn" || !response.toolCalls?.length) {
|
|
3080
|
+
finishedNormally = true;
|
|
3081
|
+
return {
|
|
3082
|
+
planText: accumulatedText,
|
|
3083
|
+
budgetExhausted,
|
|
3084
|
+
timedOut: false,
|
|
3085
|
+
finishedNormally,
|
|
3086
|
+
iterations,
|
|
3087
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
3088
|
+
};
|
|
3089
|
+
}
|
|
3090
|
+
turns.push({
|
|
3091
|
+
role: "assistant",
|
|
3092
|
+
text: response.text,
|
|
3093
|
+
toolCalls: response.toolCalls
|
|
3094
|
+
});
|
|
3095
|
+
const toolResults = [];
|
|
3096
|
+
for (const tc of response.toolCalls) {
|
|
3097
|
+
if (tc.name !== READ_FILE_TOOL.name) {
|
|
3098
|
+
toolResults.push({ toolCallId: tc.id, content: `unknown tool "${tc.name}"`, isError: true });
|
|
3099
|
+
continue;
|
|
3100
|
+
}
|
|
3101
|
+
const before = input5.budget.snapshot().bytes;
|
|
3102
|
+
const result = readFileTool(input5.root, input5.budget, tc.input);
|
|
3103
|
+
const after = input5.budget.snapshot().bytes;
|
|
3104
|
+
input5.onToolCall?.(tc, after - before, after);
|
|
3105
|
+
if (result.isError && (/budget/i.test(result.content) || /max file reads/i.test(result.content))) {
|
|
3106
|
+
budgetExhausted = true;
|
|
3107
|
+
}
|
|
3108
|
+
toolResults.push({ toolCallId: tc.id, content: result.content, isError: result.isError });
|
|
3109
|
+
}
|
|
3110
|
+
turns.push({ role: "user", toolResults });
|
|
3111
|
+
if (budgetExhausted) {
|
|
3112
|
+
turns.push({
|
|
3113
|
+
role: "user",
|
|
3114
|
+
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."
|
|
3115
|
+
});
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
return {
|
|
3119
|
+
planText: accumulatedText,
|
|
3120
|
+
budgetExhausted,
|
|
3121
|
+
timedOut: false,
|
|
3122
|
+
finishedNormally: false,
|
|
3123
|
+
iterations,
|
|
3124
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
3125
|
+
};
|
|
3126
|
+
}
|
|
3127
|
+
function firstByteDiff(a, b) {
|
|
3128
|
+
const n = Math.min(a.length, b.length);
|
|
3129
|
+
for (let i = 0; i < n; i += 1) {
|
|
3130
|
+
if (a.charCodeAt(i) !== b.charCodeAt(i)) return i;
|
|
3131
|
+
}
|
|
3132
|
+
return n;
|
|
3133
|
+
}
|
|
3134
|
+
function composePlannerError(providerName, response, retriedRateLimit, retrySkippedReason) {
|
|
3135
|
+
if (response.errorKind === "rate_limit") {
|
|
3136
|
+
return new Error(
|
|
3137
|
+
rateLimitMessage({
|
|
3138
|
+
provider: providerName,
|
|
3139
|
+
retryAfterSec: response.retryAfterSec,
|
|
3140
|
+
rawBody: response.rawError ?? "",
|
|
3141
|
+
retryAlreadyAttempted: retriedRateLimit,
|
|
3142
|
+
retrySkippedReason,
|
|
3143
|
+
maxRetrySec: MAX_RATE_LIMIT_RETRY_SEC
|
|
3144
|
+
})
|
|
3145
|
+
);
|
|
3146
|
+
}
|
|
3147
|
+
if (response.errorKind === "model_not_found") {
|
|
3148
|
+
return new Error(response.rawError ?? "planner: model not found");
|
|
3149
|
+
}
|
|
3150
|
+
const base = response.rawError ?? "planner: provider error";
|
|
3151
|
+
return new Error(`${base} Run \`squad doctor\` to diagnose, or retry \u2014 most 5xx errors are transient.`);
|
|
3152
|
+
}
|
|
3153
|
+
|
|
2848
3154
|
// src/planner/providers/index.ts
|
|
2849
3155
|
function providerFor(name) {
|
|
2850
3156
|
switch (name) {
|
|
@@ -2866,7 +3172,7 @@ var Budget = class {
|
|
|
2866
3172
|
reads = 0;
|
|
2867
3173
|
bytes = 0;
|
|
2868
3174
|
startedAt = Date.now();
|
|
2869
|
-
totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
3175
|
+
totalUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
|
|
2870
3176
|
canRead(nextFileBytes) {
|
|
2871
3177
|
if (this.reads >= this.cfg.maxFileReads) {
|
|
2872
3178
|
return { ok: false, reason: `max file reads (${this.cfg.maxFileReads}) reached` };
|
|
@@ -2884,6 +3190,8 @@ var Budget = class {
|
|
|
2884
3190
|
this.totalUsage.inputTokens += u.inputTokens;
|
|
2885
3191
|
this.totalUsage.outputTokens += u.outputTokens;
|
|
2886
3192
|
if (u.costUsd !== void 0) this.totalUsage.costUsd = (this.totalUsage.costUsd ?? 0) + u.costUsd;
|
|
3193
|
+
this.totalUsage.cacheCreationTokens = (this.totalUsage.cacheCreationTokens ?? 0) + (u.cacheCreationTokens ?? 0);
|
|
3194
|
+
this.totalUsage.cacheReadTokens = (this.totalUsage.cacheReadTokens ?? 0) + (u.cacheReadTokens ?? 0);
|
|
2887
3195
|
}
|
|
2888
3196
|
timedOut() {
|
|
2889
3197
|
return (Date.now() - this.startedAt) / 1e3 > this.cfg.maxDurationSeconds;
|
|
@@ -3029,8 +3337,8 @@ function formatSequence(n) {
|
|
|
3029
3337
|
}
|
|
3030
3338
|
|
|
3031
3339
|
// src/planner/writer.ts
|
|
3032
|
-
function writePlanFile(
|
|
3033
|
-
const { paths, config: config2, story } =
|
|
3340
|
+
function writePlanFile(input5) {
|
|
3341
|
+
const { paths, config: config2, story } = input5;
|
|
3034
3342
|
const featurePlanDir = path13.join(paths.plansDir, story.feature);
|
|
3035
3343
|
fs20.mkdirSync(featurePlanDir, { recursive: true });
|
|
3036
3344
|
const existing = findExistingPlan(featurePlanDir, story.id);
|
|
@@ -3049,9 +3357,9 @@ function writePlanFile(input6) {
|
|
|
3049
3357
|
const trackerPart = trackerId && trackerId !== baseSlug ? `-${trackerId}` : "";
|
|
3050
3358
|
targetPath = path13.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.md`);
|
|
3051
3359
|
}
|
|
3052
|
-
const body = `${
|
|
3360
|
+
const body = `${input5.metadataHeader.trimEnd()}
|
|
3053
3361
|
|
|
3054
|
-
${
|
|
3362
|
+
${input5.planBodyMarkdown.trimStart()}
|
|
3055
3363
|
`;
|
|
3056
3364
|
fs20.writeFileSync(targetPath, body, "utf8");
|
|
3057
3365
|
upsertOverviewRow(featurePlanDir, story.feature, nn, path13.basename(targetPath), story);
|
|
@@ -3118,6 +3426,45 @@ function buildMetadataHeader(args) {
|
|
|
3118
3426
|
return `<!-- squad-kit: ${parts.join(", ")} -->`;
|
|
3119
3427
|
}
|
|
3120
3428
|
|
|
3429
|
+
// src/core/last-run.ts
|
|
3430
|
+
import fs21 from "fs/promises";
|
|
3431
|
+
import path14 from "path";
|
|
3432
|
+
async function writeLastRun(paths, rec) {
|
|
3433
|
+
const file = path14.join(paths.squadDir, ".last-run.json");
|
|
3434
|
+
await fs21.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
|
|
3435
|
+
}
|
|
3436
|
+
async function readLastRun(paths) {
|
|
3437
|
+
const file = path14.join(paths.squadDir, ".last-run.json");
|
|
3438
|
+
try {
|
|
3439
|
+
const raw = await fs21.readFile(file, "utf8");
|
|
3440
|
+
const parsed = JSON.parse(raw);
|
|
3441
|
+
if (parsed.version !== 1) return void 0;
|
|
3442
|
+
return parsed;
|
|
3443
|
+
} catch {
|
|
3444
|
+
return void 0;
|
|
3445
|
+
}
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
// src/ui/planner-cache-summary.ts
|
|
3449
|
+
function formatTokenK(n) {
|
|
3450
|
+
return `${(n / 1e3).toFixed(1)}k`;
|
|
3451
|
+
}
|
|
3452
|
+
function formatPlannerCacheLine(opts) {
|
|
3453
|
+
if (!opts.cacheEnabled) {
|
|
3454
|
+
return "cache disabled";
|
|
3455
|
+
}
|
|
3456
|
+
const { stats } = opts;
|
|
3457
|
+
if (stats.cacheReadTokens > 0) {
|
|
3458
|
+
const pct = Math.round(stats.cacheHitRatio * 100);
|
|
3459
|
+
const total = stats.inputTokens + stats.cacheReadTokens;
|
|
3460
|
+
const read = formatTokenK(stats.cacheReadTokens);
|
|
3461
|
+
const tot = formatTokenK(total);
|
|
3462
|
+
const written = stats.cacheCreationTokens > 0 ? ` \xB7 ${formatTokenK(stats.cacheCreationTokens)} written` : "";
|
|
3463
|
+
return `cache hit ${pct}% (${read} read / ${tot} total${written})`;
|
|
3464
|
+
}
|
|
3465
|
+
return "cache miss 0% \u2014 run `squad doctor` to diagnose";
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3121
3468
|
// src/commands/new-plan.ts
|
|
3122
3469
|
function decideMode(opts, config2) {
|
|
3123
3470
|
if (opts.api && opts.copy) {
|
|
@@ -3149,8 +3496,8 @@ async function runNewPlan(intakePath, opts) {
|
|
|
3149
3496
|
}
|
|
3150
3497
|
}
|
|
3151
3498
|
function resolveFromPath(intakePath, stories, root) {
|
|
3152
|
-
const resolved =
|
|
3153
|
-
if (!
|
|
3499
|
+
const resolved = path15.resolve(intakePath.trim());
|
|
3500
|
+
if (!fs22.existsSync(resolved)) {
|
|
3154
3501
|
throw new Error(
|
|
3155
3502
|
`Intake not found: ${resolved}. Run \`squad list\` to see intakes, or \`squad new-story\` to create one first.`
|
|
3156
3503
|
);
|
|
@@ -3162,7 +3509,7 @@ function resolveFromPath(intakePath, stories, root) {
|
|
|
3162
3509
|
);
|
|
3163
3510
|
}
|
|
3164
3511
|
function relativeStoriesDir(root) {
|
|
3165
|
-
return
|
|
3512
|
+
return path15.relative(root, path15.join(root, ".squad", "stories"));
|
|
3166
3513
|
}
|
|
3167
3514
|
async function pickStory(stories, opts) {
|
|
3168
3515
|
const candidates = opts.all ? stories : stories.filter((s) => !s.planFile);
|
|
@@ -3193,7 +3540,7 @@ async function pickStory(stories, opts) {
|
|
|
3193
3540
|
value: s.intakePath
|
|
3194
3541
|
}))
|
|
3195
3542
|
});
|
|
3196
|
-
return candidates.find((s) =>
|
|
3543
|
+
return candidates.find((s) => path15.resolve(s.intakePath) === path15.resolve(pick));
|
|
3197
3544
|
}
|
|
3198
3545
|
function formatLabel(s) {
|
|
3199
3546
|
const head = `${s.feature} / ${s.id}`;
|
|
@@ -3254,6 +3601,7 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3254
3601
|
const userPrompt = composeUserPrompt({ intakeContent: readFile(story.intakePath) });
|
|
3255
3602
|
const sessionSpinner = { current: null };
|
|
3256
3603
|
const startedAt = Date.now();
|
|
3604
|
+
const cacheEnabled = planner.cache?.enabled ?? true;
|
|
3257
3605
|
const result = await runPlanner({
|
|
3258
3606
|
root: paths.root,
|
|
3259
3607
|
provider,
|
|
@@ -3262,6 +3610,7 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3262
3610
|
systemPrompt,
|
|
3263
3611
|
userPrompt,
|
|
3264
3612
|
budget,
|
|
3613
|
+
cacheEnabled,
|
|
3265
3614
|
onToolCall: (tc, bytes, total) => {
|
|
3266
3615
|
const p = tc.input.path ?? "<unknown>";
|
|
3267
3616
|
sessionSpinner.current?.succeed(
|
|
@@ -3304,9 +3653,18 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3304
3653
|
planBodyMarkdown: result.planText,
|
|
3305
3654
|
metadataHeader: header
|
|
3306
3655
|
});
|
|
3656
|
+
try {
|
|
3657
|
+
await writeLastRun(paths, {
|
|
3658
|
+
stats: result.stats,
|
|
3659
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3660
|
+
provider: planner.provider,
|
|
3661
|
+
model
|
|
3662
|
+
});
|
|
3663
|
+
} catch {
|
|
3664
|
+
}
|
|
3307
3665
|
blank();
|
|
3308
3666
|
summaryBox(" plan generated ", [
|
|
3309
|
-
{ key: "file", value:
|
|
3667
|
+
{ key: "file", value: path15.relative(paths.root, planFile) },
|
|
3310
3668
|
{ key: "nn", value: String(sequenceNumber).padStart(2, "0") },
|
|
3311
3669
|
{ key: "model", value: `${planner.provider}/${model}` },
|
|
3312
3670
|
{ key: "reads", value: `${snap.reads} files \xB7 ${(snap.bytes / 1024).toFixed(1)} KB` },
|
|
@@ -3314,6 +3672,7 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3314
3672
|
key: "tokens",
|
|
3315
3673
|
value: `${snap.usage.inputTokens} in \xB7 ${snap.usage.outputTokens} out${snap.usage.costUsd !== void 0 ? ` \u2248 $${snap.usage.costUsd.toFixed(2)}` : ""}`
|
|
3316
3674
|
},
|
|
3675
|
+
{ key: "cache", value: formatPlannerCacheLine({ cacheEnabled, stats: result.stats }) },
|
|
3317
3676
|
{ key: "time", value: `${Math.round(elapsedMs / 1e3)}s` },
|
|
3318
3677
|
{ key: "action", value: overwrote ? "overwrote existing plan" : "new plan" }
|
|
3319
3678
|
]);
|
|
@@ -3323,9 +3682,9 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3323
3682
|
warning("Planner did not reach end_turn. Inspect the plan carefully.");
|
|
3324
3683
|
}
|
|
3325
3684
|
blank();
|
|
3326
|
-
info(`next \u2192 open a new agent chat and attach only ${
|
|
3685
|
+
info(`next \u2192 open a new agent chat and attach only ${path15.basename(planFile)}`);
|
|
3327
3686
|
}
|
|
3328
|
-
async function emitCopyPrompt(story,
|
|
3687
|
+
async function emitCopyPrompt(story, paths, config2, clipboard) {
|
|
3329
3688
|
const intakeContent = readFile(story.intakePath);
|
|
3330
3689
|
const metaPromptTemplate = readBundledPrompt("generate-plan.md");
|
|
3331
3690
|
const composed = render(metaPromptTemplate, {
|
|
@@ -3334,13 +3693,44 @@ async function emitCopyPrompt(story, _paths, config2, clipboard) {
|
|
|
3334
3693
|
trackerType: config2.tracker.type,
|
|
3335
3694
|
intakeContent
|
|
3336
3695
|
});
|
|
3696
|
+
const promptFile = path15.join(paths.squadDir, ".last-copy-prompt.md");
|
|
3697
|
+
fs22.writeFileSync(promptFile, composed, "utf8");
|
|
3698
|
+
const relPromptFile = path15.relative(paths.root, promptFile);
|
|
3699
|
+
const bytes = Buffer.byteLength(composed, "utf8");
|
|
3700
|
+
const estTokens = Math.round(bytes / 4);
|
|
3701
|
+
const clipResult = clipboard ? await copyToClipboard(composed) : { ok: false, reason: "clipboard disabled (--no-clipboard)" };
|
|
3702
|
+
blank();
|
|
3703
|
+
divider("planner prompt ready");
|
|
3704
|
+
kv("mode", "copy-paste", 10);
|
|
3705
|
+
kv("story", `${story.feature} / ${story.id}`, 10);
|
|
3706
|
+
kv("prompt", relPromptFile, 10);
|
|
3707
|
+
kv("size", `${formatKB(bytes)} \xB7 ~${formatCount(estTokens)} tokens (est)`, 10);
|
|
3708
|
+
if (clipResult.ok) {
|
|
3709
|
+
kv("clipboard", `\u2713 copied via ${clipResult.tool}`, 10);
|
|
3710
|
+
} else {
|
|
3711
|
+
kv("clipboard", `! ${clipResult.reason}`, 10);
|
|
3712
|
+
}
|
|
3337
3713
|
printModelBanner(config2);
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3714
|
+
blank();
|
|
3715
|
+
step("Next:");
|
|
3716
|
+
info("1) Open your agent chat (Cursor / Claude Code / Copilot / Gemini).");
|
|
3717
|
+
info("2) Switch to a strong plan model (Opus 4.x / GPT-5.3 Codex thinking / Gemini deep-think).");
|
|
3718
|
+
if (clipResult.ok) {
|
|
3719
|
+
info("3) Paste from clipboard and let the agent work. It will read files and write the plan itself.");
|
|
3720
|
+
} else {
|
|
3721
|
+
info(`3) Open ${relPromptFile}, paste its contents into the agent chat, and let it work.`);
|
|
3343
3722
|
}
|
|
3723
|
+
info(
|
|
3724
|
+
`4) The agent writes the plan to .squad/plans/${story.feature}/<nn>-story-${story.id}.md. Review it, then open a fresh chat to implement (attach only the plan file).`
|
|
3725
|
+
);
|
|
3726
|
+
}
|
|
3727
|
+
function formatKB(bytes) {
|
|
3728
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
3729
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
3730
|
+
}
|
|
3731
|
+
function formatCount(n) {
|
|
3732
|
+
if (n < 1e3) return String(n);
|
|
3733
|
+
return `${(n / 1e3).toFixed(1)}k`;
|
|
3344
3734
|
}
|
|
3345
3735
|
function printModelBanner(config2) {
|
|
3346
3736
|
if (!isInteractive()) return;
|
|
@@ -3363,7 +3753,7 @@ function printModelBanner(config2) {
|
|
|
3363
3753
|
}
|
|
3364
3754
|
|
|
3365
3755
|
// src/commands/status.ts
|
|
3366
|
-
import
|
|
3756
|
+
import fs23 from "fs";
|
|
3367
3757
|
var kw = 28;
|
|
3368
3758
|
function probeTrackerCredential(config2, overlay) {
|
|
3369
3759
|
const { client, error: error2 } = clientFor(config2, overlay);
|
|
@@ -3389,7 +3779,7 @@ async function runStatus() {
|
|
|
3389
3779
|
const scan = scanPlans(paths.plansDir);
|
|
3390
3780
|
const storyCount = listStories(paths).length;
|
|
3391
3781
|
const planCount = scan.usedNumbers.length;
|
|
3392
|
-
const secrets =
|
|
3782
|
+
const secrets = fs23.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
3393
3783
|
const overlay = overlayTrackerEnv(secrets);
|
|
3394
3784
|
banner();
|
|
3395
3785
|
step("squad-kit status");
|
|
@@ -3443,13 +3833,13 @@ function buildTrackerLine(config2, overlay) {
|
|
|
3443
3833
|
}
|
|
3444
3834
|
|
|
3445
3835
|
// src/commands/list.ts
|
|
3446
|
-
import
|
|
3836
|
+
import path16 from "path";
|
|
3447
3837
|
async function runList(opts) {
|
|
3448
3838
|
const root = requireSquadRoot();
|
|
3449
3839
|
const paths = buildPaths(root);
|
|
3450
3840
|
const records = listStories(paths, { feature: opts.feature });
|
|
3451
3841
|
const rows = records.map((r) => {
|
|
3452
|
-
const planAbs = r.planFile ?
|
|
3842
|
+
const planAbs = r.planFile ? path16.join(paths.plansDir, r.feature, r.planFile) : void 0;
|
|
3453
3843
|
const meta = planAbs ? readPlanMetadata(planAbs) : void 0;
|
|
3454
3844
|
return {
|
|
3455
3845
|
feature: r.feature,
|
|
@@ -3488,9 +3878,9 @@ function stripAnsi(s) {
|
|
|
3488
3878
|
}
|
|
3489
3879
|
|
|
3490
3880
|
// src/commands/tracker-link.ts
|
|
3491
|
-
import
|
|
3492
|
-
import
|
|
3493
|
-
import { input as
|
|
3881
|
+
import fs24 from "fs";
|
|
3882
|
+
import path17 from "path";
|
|
3883
|
+
import { input as input4, select as select6 } from "@inquirer/prompts";
|
|
3494
3884
|
async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
3495
3885
|
const root = requireSquadRoot();
|
|
3496
3886
|
const paths = buildPaths(root);
|
|
@@ -3522,7 +3912,7 @@ async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
|
3522
3912
|
"Usage: squad tracker link [story-path] [tracker-id]. Run `squad tracker link` without arguments in a TTY to pick from a list, or pass `--yes` to opt out of prompts."
|
|
3523
3913
|
);
|
|
3524
3914
|
}
|
|
3525
|
-
resolvedTrackerId = (await
|
|
3915
|
+
resolvedTrackerId = (await input4({
|
|
3526
3916
|
message: `${config2.tracker.type} work-item id:`,
|
|
3527
3917
|
validate: (v) => {
|
|
3528
3918
|
const trimmed = v.trim();
|
|
@@ -3536,17 +3926,21 @@ async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
|
3536
3926
|
`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.`
|
|
3537
3927
|
);
|
|
3538
3928
|
}
|
|
3539
|
-
const resolved =
|
|
3540
|
-
const intakeFile = resolved.endsWith("intake.md") ? resolved :
|
|
3541
|
-
if (!
|
|
3929
|
+
const resolved = path17.resolve(resolvedStoryPath);
|
|
3930
|
+
const intakeFile = resolved.endsWith("intake.md") ? resolved : path17.join(resolved, "intake.md");
|
|
3931
|
+
if (!fs24.existsSync(intakeFile)) {
|
|
3542
3932
|
throw new Error(
|
|
3543
3933
|
`intake.md not found at ${intakeFile}. Run \`squad list\` to see valid paths, or \`squad new-story\` to create a story first.`
|
|
3544
3934
|
);
|
|
3545
3935
|
}
|
|
3546
|
-
const content =
|
|
3936
|
+
const content = fs24.readFileSync(intakeFile, "utf8");
|
|
3547
3937
|
const updated = upsertTrackerId(content, resolvedTrackerId);
|
|
3548
|
-
|
|
3549
|
-
success(`Linked tracker id ${resolvedTrackerId} \u2192 ${
|
|
3938
|
+
fs24.writeFileSync(intakeFile, updated, "utf8");
|
|
3939
|
+
success(`Linked tracker id ${resolvedTrackerId} \u2192 ${path17.relative(root, intakeFile)}`);
|
|
3940
|
+
blank();
|
|
3941
|
+
step("Next:");
|
|
3942
|
+
info("1) Review the intake \u2014 the Source block at the top now carries the tracker id.");
|
|
3943
|
+
info("2) Run `squad new-plan --api` (or `--copy`) to generate the plan for this story.");
|
|
3550
3944
|
}
|
|
3551
3945
|
function upsertTrackerId(content, id) {
|
|
3552
3946
|
const pattern = /(-\s*\*\*Work item id:\*\*)[^\n]*/;
|
|
@@ -3560,8 +3954,8 @@ function upsertTrackerId(content, id) {
|
|
|
3560
3954
|
}
|
|
3561
3955
|
|
|
3562
3956
|
// src/commands/doctor.ts
|
|
3563
|
-
import
|
|
3564
|
-
import
|
|
3957
|
+
import fs25 from "fs";
|
|
3958
|
+
import path18 from "path";
|
|
3565
3959
|
async function gatherContext(paths) {
|
|
3566
3960
|
const ctx = { hasLegacyPromptsDir: false };
|
|
3567
3961
|
try {
|
|
@@ -3570,29 +3964,29 @@ async function gatherContext(paths) {
|
|
|
3570
3964
|
ctx.configError = err;
|
|
3571
3965
|
}
|
|
3572
3966
|
try {
|
|
3573
|
-
ctx.secrets =
|
|
3967
|
+
ctx.secrets = fs25.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
3574
3968
|
} catch (err) {
|
|
3575
3969
|
ctx.secretsError = err;
|
|
3576
3970
|
}
|
|
3577
|
-
ctx.hasLegacyPromptsDir =
|
|
3971
|
+
ctx.hasLegacyPromptsDir = fs25.existsSync(paths.promptsDir);
|
|
3578
3972
|
return ctx;
|
|
3579
3973
|
}
|
|
3580
3974
|
function gitignoreHasManagedBlock(repoRoot) {
|
|
3581
|
-
const gitignore =
|
|
3582
|
-
return
|
|
3975
|
+
const gitignore = path18.join(repoRoot, ".gitignore");
|
|
3976
|
+
return fs25.existsSync(gitignore) && fs25.readFileSync(gitignore, "utf8").includes(".squad/secrets.yaml");
|
|
3583
3977
|
}
|
|
3584
3978
|
function gitignoreHasTrashPattern(repoRoot) {
|
|
3585
|
-
const gitignore =
|
|
3586
|
-
return
|
|
3979
|
+
const gitignore = path18.join(repoRoot, ".gitignore");
|
|
3980
|
+
return fs25.existsSync(gitignore) && fs25.readFileSync(gitignore, "utf8").includes(SQUAD_TRASH_PATTERN);
|
|
3587
3981
|
}
|
|
3588
3982
|
async function checkDirStructure(paths, _ctx, fix) {
|
|
3589
|
-
const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !
|
|
3983
|
+
const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !fs25.existsSync(p));
|
|
3590
3984
|
if (need.length === 0) {
|
|
3591
3985
|
return { id: "dirs", name: ".squad/ directory structure", status: "ok" };
|
|
3592
3986
|
}
|
|
3593
3987
|
if (fix) {
|
|
3594
3988
|
for (const p of need) {
|
|
3595
|
-
|
|
3989
|
+
fs25.mkdirSync(p, { recursive: true });
|
|
3596
3990
|
}
|
|
3597
3991
|
return { id: "dirs", name: ".squad/ directory structure", status: "ok", detail: "repaired" };
|
|
3598
3992
|
}
|
|
@@ -3600,7 +3994,7 @@ async function checkDirStructure(paths, _ctx, fix) {
|
|
|
3600
3994
|
id: "dirs",
|
|
3601
3995
|
name: ".squad/ directory structure",
|
|
3602
3996
|
status: "warn",
|
|
3603
|
-
detail: `missing: ${need.map((p) =>
|
|
3997
|
+
detail: `missing: ${need.map((p) => path18.relative(paths.root, p)).join(", ")}`,
|
|
3604
3998
|
fixable: true,
|
|
3605
3999
|
fixHint: "squad doctor --fix"
|
|
3606
4000
|
};
|
|
@@ -3655,15 +4049,15 @@ async function checkSecretsPermissions(paths, _ctx, fix) {
|
|
|
3655
4049
|
if (process.platform === "win32") {
|
|
3656
4050
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "skip", detail: "Windows" };
|
|
3657
4051
|
}
|
|
3658
|
-
if (!
|
|
4052
|
+
if (!fs25.existsSync(paths.secretsFile)) {
|
|
3659
4053
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "not present" };
|
|
3660
4054
|
}
|
|
3661
|
-
const mode =
|
|
4055
|
+
const mode = fs25.statSync(paths.secretsFile).mode & 511;
|
|
3662
4056
|
if (mode === 384) {
|
|
3663
4057
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok" };
|
|
3664
4058
|
}
|
|
3665
4059
|
if (fix) {
|
|
3666
|
-
|
|
4060
|
+
fs25.chmodSync(paths.secretsFile, 384);
|
|
3667
4061
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "repaired" };
|
|
3668
4062
|
}
|
|
3669
4063
|
return {
|
|
@@ -3684,7 +4078,7 @@ async function checkSecretsParseable(_paths, ctx) {
|
|
|
3684
4078
|
detail: ctx.secretsError.message
|
|
3685
4079
|
};
|
|
3686
4080
|
}
|
|
3687
|
-
if (!
|
|
4081
|
+
if (!fs25.existsSync(_paths.secretsFile)) {
|
|
3688
4082
|
return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok", detail: "not present" };
|
|
3689
4083
|
}
|
|
3690
4084
|
return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok" };
|
|
@@ -3849,6 +4243,19 @@ async function checkPlannerTierAwareness(_paths, ctx) {
|
|
|
3849
4243
|
if (!/opus/i.test(planModel)) {
|
|
3850
4244
|
return { id, name, status: "ok", detail: `${planModel} is comfortably under Tier 1` };
|
|
3851
4245
|
}
|
|
4246
|
+
const cacheOn = ctx.config.planner?.cache?.enabled !== false;
|
|
4247
|
+
if (cacheOn) {
|
|
4248
|
+
return {
|
|
4249
|
+
id,
|
|
4250
|
+
name,
|
|
4251
|
+
status: "warn",
|
|
4252
|
+
detail: "Anthropic Tier 1 with Opus \u2014 tight but viable with prompt caching on",
|
|
4253
|
+
fixHint: [
|
|
4254
|
+
"Caching saves ~70% on billed input tokens. Keep `planner.cache.enabled: true` in config.",
|
|
4255
|
+
"If you still hit 429s on long runs, reduce `planner.budget.maxContextBytes` or use Haiku for plan phase."
|
|
4256
|
+
].join("\n")
|
|
4257
|
+
};
|
|
4258
|
+
}
|
|
3852
4259
|
return {
|
|
3853
4260
|
id,
|
|
3854
4261
|
name,
|
|
@@ -3857,6 +4264,66 @@ async function checkPlannerTierAwareness(_paths, ctx) {
|
|
|
3857
4264
|
fixHint: "Run `squad config set planner` and pick a Sonnet or Haiku id for planner.modelOverride.anthropic, or upgrade tier at https://console.anthropic.com/settings/limits. squad-kit will auto-retry a 429 once (waiting up to 90s), but repeated throttling means the plan model is simply too big for your quota."
|
|
3858
4265
|
};
|
|
3859
4266
|
}
|
|
4267
|
+
async function checkPlannerCache(paths, ctx) {
|
|
4268
|
+
const id = "planner-cache";
|
|
4269
|
+
const name = "planner cache effectiveness";
|
|
4270
|
+
const config2 = ctx.config;
|
|
4271
|
+
const cacheCfg = config2?.planner?.cache;
|
|
4272
|
+
if (config2?.planner?.enabled !== true) {
|
|
4273
|
+
return { id, name, status: "skip", detail: "planner disabled \u2014 cache check not applicable" };
|
|
4274
|
+
}
|
|
4275
|
+
if (cacheCfg?.enabled === false) {
|
|
4276
|
+
return {
|
|
4277
|
+
id,
|
|
4278
|
+
name,
|
|
4279
|
+
status: "warn",
|
|
4280
|
+
detail: "prompt caching is disabled in .squad/config.yaml",
|
|
4281
|
+
fixHint: "Cache saves ~70% on billed tokens. Re-enable with `squad config set planner`."
|
|
4282
|
+
};
|
|
4283
|
+
}
|
|
4284
|
+
const lastRun = await readLastRun(paths);
|
|
4285
|
+
if (!lastRun) {
|
|
4286
|
+
return {
|
|
4287
|
+
id,
|
|
4288
|
+
name,
|
|
4289
|
+
status: "skip",
|
|
4290
|
+
detail: "no planner runs logged yet",
|
|
4291
|
+
fixHint: "Run `squad new-plan --api` once, then re-run `squad doctor` to see cache telemetry."
|
|
4292
|
+
};
|
|
4293
|
+
}
|
|
4294
|
+
const { stats } = lastRun;
|
|
4295
|
+
const hitPct = Math.round(stats.cacheHitRatio * 100);
|
|
4296
|
+
if (stats.cacheReadTokens === 0 && stats.turns > 1) {
|
|
4297
|
+
return {
|
|
4298
|
+
id,
|
|
4299
|
+
name,
|
|
4300
|
+
status: "fail",
|
|
4301
|
+
detail: `caching configured but last run saw 0% hits across ${stats.turns} turns`,
|
|
4302
|
+
fixHint: [
|
|
4303
|
+
"Possible causes:",
|
|
4304
|
+
" \u2022 Planner provider is not Anthropic and the prefix is below 1024 tokens (OpenAI / Google need larger prefixes).",
|
|
4305
|
+
" \u2022 System prompt is mutating between turns \u2014 check recent changes.",
|
|
4306
|
+
" \u2022 Using an older model that doesn't support caching.",
|
|
4307
|
+
`Provider: ${lastRun.provider}/${lastRun.model}. Run \`NODE_ENV=development squad new-plan --api\` to surface prefix-mismatch warnings.`
|
|
4308
|
+
].join("\n")
|
|
4309
|
+
};
|
|
4310
|
+
}
|
|
4311
|
+
if (hitPct < 30 && stats.turns > 3) {
|
|
4312
|
+
return {
|
|
4313
|
+
id,
|
|
4314
|
+
name,
|
|
4315
|
+
status: "warn",
|
|
4316
|
+
detail: `low cache hit rate: ${hitPct}% (last run, ${stats.turns} turns)`,
|
|
4317
|
+
fixHint: "Expected \u226560% for Anthropic, \u226540% for OpenAI, \u226550% for Google after 3+ turns. Prefix may be unstable."
|
|
4318
|
+
};
|
|
4319
|
+
}
|
|
4320
|
+
return {
|
|
4321
|
+
id,
|
|
4322
|
+
name,
|
|
4323
|
+
status: "ok",
|
|
4324
|
+
detail: `caching active \u2014 last run ${hitPct}% hit (${formatTokenK(stats.cacheReadTokens)} read, ${lastRun.provider}/${lastRun.model})`
|
|
4325
|
+
};
|
|
4326
|
+
}
|
|
3860
4327
|
async function checkTrackerConfig(_paths, ctx) {
|
|
3861
4328
|
if (!ctx.config) {
|
|
3862
4329
|
return { id: "tracker-config", name: "tracker configuration", status: "skip", detail: "config unavailable" };
|
|
@@ -3979,6 +4446,7 @@ async function runAllChecks(paths, ctx, fix) {
|
|
|
3979
4446
|
await add("planner credential resolves", () => checkPlannerCredential(paths, ctx));
|
|
3980
4447
|
await add("planner model resolves at provider", () => checkPlannerModel(paths, ctx));
|
|
3981
4448
|
await add("planner tier vs. model", () => checkPlannerTierAwareness(paths, ctx));
|
|
4449
|
+
await add("planner cache effectiveness", () => checkPlannerCache(paths, ctx));
|
|
3982
4450
|
await add("tracker configuration", () => checkTrackerConfig(paths, ctx));
|
|
3983
4451
|
await add("tracker credential resolves", () => checkTrackerCredential(paths, ctx));
|
|
3984
4452
|
await add("tracker connectivity", () => checkTrackerConnectivity(paths, ctx));
|
|
@@ -4084,23 +4552,23 @@ async function runDoctor(opts) {
|
|
|
4084
4552
|
}
|
|
4085
4553
|
|
|
4086
4554
|
// src/commands/migrate.ts
|
|
4087
|
-
import
|
|
4088
|
-
import
|
|
4555
|
+
import fs26 from "fs";
|
|
4556
|
+
import path19 from "path";
|
|
4089
4557
|
import { confirm as confirm7 } from "@inquirer/prompts";
|
|
4090
4558
|
var LEGACY_PROMPTS_MIGRATION = {
|
|
4091
4559
|
id: "legacy-prompts",
|
|
4092
4560
|
name: "Remove legacy .squad/prompts/",
|
|
4093
|
-
isApplied: (paths) => !
|
|
4094
|
-
describe: (paths) => `delete ${
|
|
4561
|
+
isApplied: (paths) => !fs26.existsSync(paths.promptsDir),
|
|
4562
|
+
describe: (paths) => `delete ${path19.relative(paths.root, paths.promptsDir)} (prompts are now bundled in squad-kit)`,
|
|
4095
4563
|
apply: (paths) => {
|
|
4096
4564
|
const target = paths.promptsDir;
|
|
4097
|
-
if (!target.includes(
|
|
4565
|
+
if (!target.includes(path19.sep + ".squad" + path19.sep + "prompts")) {
|
|
4098
4566
|
throw new Error(
|
|
4099
4567
|
`Refusing to delete suspicious path: ${target}. This is a safety check \u2014 run \`squad doctor\` if .squad/ paths look wrong.`
|
|
4100
4568
|
);
|
|
4101
4569
|
}
|
|
4102
|
-
if (
|
|
4103
|
-
|
|
4570
|
+
if (fs26.existsSync(target)) {
|
|
4571
|
+
fs26.rmSync(target, { recursive: true, force: true });
|
|
4104
4572
|
}
|
|
4105
4573
|
}
|
|
4106
4574
|
};
|
|
@@ -4108,8 +4576,8 @@ var GITIGNORE_MIGRATION = {
|
|
|
4108
4576
|
id: "gitignore",
|
|
4109
4577
|
name: "Ensure .gitignore managed block",
|
|
4110
4578
|
isApplied: (paths) => {
|
|
4111
|
-
const f =
|
|
4112
|
-
return
|
|
4579
|
+
const f = path19.join(paths.root, ".gitignore");
|
|
4580
|
+
return fs26.existsSync(f) && fs26.readFileSync(f, "utf8").includes(".squad/secrets.yaml");
|
|
4113
4581
|
},
|
|
4114
4582
|
describe: () => "add squad-kit managed block to .gitignore",
|
|
4115
4583
|
apply: (paths) => {
|
|
@@ -4121,23 +4589,23 @@ var SECRETS_PERMS_MIGRATION = {
|
|
|
4121
4589
|
name: "Tighten .squad/secrets.yaml permissions",
|
|
4122
4590
|
isApplied: (paths) => {
|
|
4123
4591
|
if (process.platform === "win32") return true;
|
|
4124
|
-
if (!
|
|
4125
|
-
const st =
|
|
4592
|
+
if (!fs26.existsSync(paths.secretsFile)) return true;
|
|
4593
|
+
const st = fs26.statSync(paths.secretsFile);
|
|
4126
4594
|
return (st.mode & 511) === 384;
|
|
4127
4595
|
},
|
|
4128
|
-
describe: (paths) => `chmod 0600 ${
|
|
4596
|
+
describe: (paths) => `chmod 0600 ${path19.relative(paths.root, paths.secretsFile)}`,
|
|
4129
4597
|
apply: (paths) => {
|
|
4130
4598
|
if (process.platform === "win32") return;
|
|
4131
|
-
if (
|
|
4599
|
+
if (fs26.existsSync(paths.secretsFile)) fs26.chmodSync(paths.secretsFile, 384);
|
|
4132
4600
|
}
|
|
4133
4601
|
};
|
|
4134
4602
|
var PLANNER_BUDGET_DEFAULTS_MIGRATION = {
|
|
4135
4603
|
id: "planner-budget-defaults",
|
|
4136
4604
|
name: "Backfill planner.budget defaults in config.yaml",
|
|
4137
4605
|
isApplied: (paths) => {
|
|
4138
|
-
if (!
|
|
4606
|
+
if (!fs26.existsSync(paths.configFile)) return true;
|
|
4139
4607
|
try {
|
|
4140
|
-
const raw =
|
|
4608
|
+
const raw = fs26.readFileSync(paths.configFile, "utf8");
|
|
4141
4609
|
if (!/^\s*planner:/m.test(raw)) return true;
|
|
4142
4610
|
if (/^\s*budget:/m.test(raw)) return true;
|
|
4143
4611
|
const cfg = loadConfig(paths.configFile);
|
|
@@ -4156,9 +4624,9 @@ var CONFIG_NORMALIZE_MIGRATION = {
|
|
|
4156
4624
|
id: "config-normalize",
|
|
4157
4625
|
name: "Normalise .squad/config.yaml formatting",
|
|
4158
4626
|
isApplied: (paths) => {
|
|
4159
|
-
if (!
|
|
4627
|
+
if (!fs26.existsSync(paths.configFile)) return true;
|
|
4160
4628
|
try {
|
|
4161
|
-
const raw =
|
|
4629
|
+
const raw = fs26.readFileSync(paths.configFile, "utf8");
|
|
4162
4630
|
const cfg = loadConfig(paths.configFile);
|
|
4163
4631
|
return raw.trim() === serializeConfig(cfg).trim();
|
|
4164
4632
|
} catch {
|
|
@@ -4167,7 +4635,7 @@ var CONFIG_NORMALIZE_MIGRATION = {
|
|
|
4167
4635
|
},
|
|
4168
4636
|
describe: () => "rewrite config.yaml with canonical YAML formatting (drops comments)",
|
|
4169
4637
|
apply: (paths) => {
|
|
4170
|
-
if (!
|
|
4638
|
+
if (!fs26.existsSync(paths.configFile)) return;
|
|
4171
4639
|
const cfg = loadConfig(paths.configFile);
|
|
4172
4640
|
saveConfig(paths.configFile, cfg);
|
|
4173
4641
|
}
|
|
@@ -4235,7 +4703,11 @@ async function runMigrate(opts) {
|
|
|
4235
4703
|
}
|
|
4236
4704
|
info("");
|
|
4237
4705
|
success(`Applied ${applied}/${pending.length} migration(s).`);
|
|
4238
|
-
|
|
4706
|
+
blank();
|
|
4707
|
+
step("Next:");
|
|
4708
|
+
info("1) Run `squad doctor` to verify every check is green.");
|
|
4709
|
+
info("2) If you were upgrading from 0.1.x, review .squad/config.yaml \u2014 new defaults (planner, cache) may have been added.");
|
|
4710
|
+
info("3) Continue your usual workflow: `squad new-story` \u2192 fill intake \u2192 `squad new-plan`.");
|
|
4239
4711
|
}
|
|
4240
4712
|
|
|
4241
4713
|
// src/commands/upgrade.ts
|
|
@@ -4244,8 +4716,8 @@ import process3 from "process";
|
|
|
4244
4716
|
import { confirm as confirm8 } from "@inquirer/prompts";
|
|
4245
4717
|
|
|
4246
4718
|
// src/core/package-info.ts
|
|
4247
|
-
import
|
|
4248
|
-
import
|
|
4719
|
+
import fs27 from "fs";
|
|
4720
|
+
import path20 from "path";
|
|
4249
4721
|
function changelogBlobUrl(repositoryUrl, version) {
|
|
4250
4722
|
let base = repositoryUrl;
|
|
4251
4723
|
if (!base) base = "https://github.com/AzmSquad/squad-kit";
|
|
@@ -4256,9 +4728,9 @@ function changelogBlobUrl(repositoryUrl, version) {
|
|
|
4256
4728
|
}
|
|
4257
4729
|
function readInstalledPackage() {
|
|
4258
4730
|
const root = packageRoot();
|
|
4259
|
-
const pkgFile =
|
|
4260
|
-
const pkg = JSON.parse(
|
|
4261
|
-
const isDevInstall =
|
|
4731
|
+
const pkgFile = path20.join(root, "package.json");
|
|
4732
|
+
const pkg = JSON.parse(fs27.readFileSync(pkgFile, "utf8"));
|
|
4733
|
+
const isDevInstall = fs27.existsSync(path20.join(root, "src"));
|
|
4262
4734
|
return {
|
|
4263
4735
|
name: pkg.name,
|
|
4264
4736
|
version: pkg.version,
|
|
@@ -4298,7 +4770,7 @@ function isNewer(a, b) {
|
|
|
4298
4770
|
}
|
|
4299
4771
|
|
|
4300
4772
|
// src/core/package-manager.ts
|
|
4301
|
-
import
|
|
4773
|
+
import path21 from "path";
|
|
4302
4774
|
import process2 from "process";
|
|
4303
4775
|
function detectPackageManager(installRoot) {
|
|
4304
4776
|
const ua = process2.env.npm_config_user_agent;
|
|
@@ -4309,11 +4781,11 @@ function detectPackageManager(installRoot) {
|
|
|
4309
4781
|
if (ua.startsWith("npm/")) return { pm: "npm", reason: "npm_config_user_agent" };
|
|
4310
4782
|
}
|
|
4311
4783
|
const root = installRoot.toLowerCase();
|
|
4312
|
-
if (root.includes(`${
|
|
4784
|
+
if (root.includes(`${path21.sep}pnpm${path21.sep}`) || root.includes(`${path21.sep}.pnpm${path21.sep}`))
|
|
4313
4785
|
return { pm: "pnpm", reason: `install path contains pnpm` };
|
|
4314
|
-
if (root.includes(`${
|
|
4786
|
+
if (root.includes(`${path21.sep}.yarn${path21.sep}`) || root.includes(`${path21.sep}yarn${path21.sep}`))
|
|
4315
4787
|
return { pm: "yarn", reason: `install path contains yarn` };
|
|
4316
|
-
if (root.includes(`${
|
|
4788
|
+
if (root.includes(`${path21.sep}.bun${path21.sep}`) || root.includes(`${path21.sep}bun${path21.sep}`))
|
|
4317
4789
|
return { pm: "bun", reason: `install path contains bun` };
|
|
4318
4790
|
return { pm: "npm", reason: "default fallback" };
|
|
4319
4791
|
}
|
|
@@ -4414,8 +4886,8 @@ function spawnInherit(cmd, args) {
|
|
|
4414
4886
|
}
|
|
4415
4887
|
|
|
4416
4888
|
// src/commands/rm/shared.ts
|
|
4417
|
-
import
|
|
4418
|
-
import
|
|
4889
|
+
import fs28 from "fs";
|
|
4890
|
+
import path22 from "path";
|
|
4419
4891
|
import { select as select7 } from "@inquirer/prompts";
|
|
4420
4892
|
var STORY_PLAN = /^(\d{2,})-story-.+\.md$/;
|
|
4421
4893
|
function canPromptForRm(opts) {
|
|
@@ -4423,9 +4895,9 @@ function canPromptForRm(opts) {
|
|
|
4423
4895
|
return isInteractive() && Boolean(process.stdin.isTTY);
|
|
4424
4896
|
}
|
|
4425
4897
|
function removeOverviewRow(featurePlanDir, storyId) {
|
|
4426
|
-
const overview =
|
|
4427
|
-
if (!
|
|
4428
|
-
const src =
|
|
4898
|
+
const overview = path22.join(featurePlanDir, "00-overview.md");
|
|
4899
|
+
if (!fs28.existsSync(overview)) return;
|
|
4900
|
+
const src = fs28.readFileSync(overview, "utf8");
|
|
4429
4901
|
const lines = src.split("\n");
|
|
4430
4902
|
const filtered = lines.filter((line2) => {
|
|
4431
4903
|
if (!line2.startsWith("|")) return true;
|
|
@@ -4433,12 +4905,12 @@ function removeOverviewRow(featurePlanDir, storyId) {
|
|
|
4433
4905
|
if (/^\|\s*-+/.test(line2)) return true;
|
|
4434
4906
|
return !line2.includes(storyId);
|
|
4435
4907
|
});
|
|
4436
|
-
|
|
4908
|
+
fs28.writeFileSync(overview, filtered.join("\n"), "utf8");
|
|
4437
4909
|
}
|
|
4438
4910
|
function removeOverviewRowForPlanFile(featurePlanDir, planBasename) {
|
|
4439
|
-
const overview =
|
|
4440
|
-
if (!
|
|
4441
|
-
const src =
|
|
4911
|
+
const overview = path22.join(featurePlanDir, "00-overview.md");
|
|
4912
|
+
if (!fs28.existsSync(overview)) return;
|
|
4913
|
+
const src = fs28.readFileSync(overview, "utf8");
|
|
4442
4914
|
const token = `\`${planBasename}\``;
|
|
4443
4915
|
const lines = src.split("\n");
|
|
4444
4916
|
const filtered = lines.filter((line2) => {
|
|
@@ -4447,25 +4919,25 @@ function removeOverviewRowForPlanFile(featurePlanDir, planBasename) {
|
|
|
4447
4919
|
if (/^\|\s*-+/.test(line2)) return true;
|
|
4448
4920
|
return !line2.includes(token);
|
|
4449
4921
|
});
|
|
4450
|
-
|
|
4922
|
+
fs28.writeFileSync(overview, filtered.join("\n"), "utf8");
|
|
4451
4923
|
}
|
|
4452
4924
|
function trashOrDelete(absPaths, trashRoot, useTrash) {
|
|
4453
4925
|
if (!useTrash) {
|
|
4454
|
-
for (const p of absPaths)
|
|
4926
|
+
for (const p of absPaths) fs28.rmSync(p, { recursive: true, force: true });
|
|
4455
4927
|
return { removed: absPaths };
|
|
4456
4928
|
}
|
|
4457
4929
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4458
|
-
const bucket =
|
|
4459
|
-
|
|
4930
|
+
const bucket = path22.join(trashRoot, ts);
|
|
4931
|
+
fs28.mkdirSync(bucket, { recursive: true });
|
|
4460
4932
|
for (const src of absPaths) {
|
|
4461
|
-
const rel =
|
|
4462
|
-
let dst =
|
|
4933
|
+
const rel = path22.basename(src);
|
|
4934
|
+
let dst = path22.join(bucket, rel);
|
|
4463
4935
|
let n = 0;
|
|
4464
|
-
while (
|
|
4936
|
+
while (fs28.existsSync(dst)) {
|
|
4465
4937
|
n += 1;
|
|
4466
|
-
dst =
|
|
4938
|
+
dst = path22.join(bucket, `${n}_${rel}`);
|
|
4467
4939
|
}
|
|
4468
|
-
|
|
4940
|
+
fs28.renameSync(src, dst);
|
|
4469
4941
|
}
|
|
4470
4942
|
return { removed: absPaths, trashed: bucket };
|
|
4471
4943
|
}
|
|
@@ -4476,21 +4948,21 @@ function renderPreview(kind, targets, opts) {
|
|
|
4476
4948
|
kv(t.kind, t.label, 6);
|
|
4477
4949
|
}
|
|
4478
4950
|
if (opts.trash) {
|
|
4479
|
-
info(`Items will move to ${
|
|
4951
|
+
info(`Items will move to ${path22.relative(process.cwd(), opts.trashRoot)}/<timestamp>/`);
|
|
4480
4952
|
} else if (!opts.dryRun) {
|
|
4481
4953
|
warning("This is a permanent delete. Pass --trash to move to .squad/.trash/ instead.");
|
|
4482
4954
|
}
|
|
4483
4955
|
}
|
|
4484
4956
|
function listAllPlanListEntries(paths, featureFilter) {
|
|
4485
|
-
if (!
|
|
4957
|
+
if (!fs28.existsSync(paths.plansDir)) return [];
|
|
4486
4958
|
const out = [];
|
|
4487
|
-
for (const ent of
|
|
4959
|
+
for (const ent of fs28.readdirSync(paths.plansDir, { withFileTypes: true })) {
|
|
4488
4960
|
if (!ent.isDirectory()) continue;
|
|
4489
4961
|
if (featureFilter && ent.name !== featureFilter) continue;
|
|
4490
|
-
const featureDir =
|
|
4491
|
-
for (const name of
|
|
4962
|
+
const featureDir = path22.join(paths.plansDir, ent.name);
|
|
4963
|
+
for (const name of fs28.readdirSync(featureDir)) {
|
|
4492
4964
|
if (!STORY_PLAN.test(name)) continue;
|
|
4493
|
-
out.push({ feature: ent.name, file: name, abs:
|
|
4965
|
+
out.push({ feature: ent.name, file: name, abs: path22.join(featureDir, name) });
|
|
4494
4966
|
}
|
|
4495
4967
|
}
|
|
4496
4968
|
return out.sort((a, b) => (a.feature + a.file).localeCompare(b.feature + b.file));
|
|
@@ -4541,34 +5013,34 @@ function resolveStoryFromArg(arg, stories, root) {
|
|
|
4541
5013
|
if (s.id === t) return s;
|
|
4542
5014
|
if (`${s.feature}/${s.id}` === t) return s;
|
|
4543
5015
|
}
|
|
4544
|
-
const abs =
|
|
4545
|
-
const withIntake = t.endsWith("intake.md") ? abs :
|
|
5016
|
+
const abs = path22.isAbsolute(t) ? t : path22.resolve(root, t);
|
|
5017
|
+
const withIntake = t.endsWith("intake.md") ? abs : path22.join(abs, "intake.md");
|
|
4546
5018
|
return findStoryByIntake(stories, withIntake) ?? findStoryByIntake(stories, abs);
|
|
4547
5019
|
}
|
|
4548
5020
|
function isSequenceToken(s) {
|
|
4549
5021
|
return /^\d{1,4}$/.test(s) && s.length > 0;
|
|
4550
5022
|
}
|
|
4551
5023
|
function tryParsePlanFilePath(absPath, paths, featureFilter) {
|
|
4552
|
-
if (!
|
|
4553
|
-
const planRoot =
|
|
4554
|
-
const fileReal =
|
|
4555
|
-
const rel =
|
|
4556
|
-
if (rel.startsWith("..") ||
|
|
4557
|
-
const segs = rel.split(
|
|
5024
|
+
if (!fs28.existsSync(absPath) || !STORY_PLAN.test(path22.basename(absPath))) return void 0;
|
|
5025
|
+
const planRoot = fs28.realpathSync(path22.resolve(paths.plansDir));
|
|
5026
|
+
const fileReal = fs28.realpathSync(path22.resolve(absPath));
|
|
5027
|
+
const rel = path22.relative(planRoot, fileReal);
|
|
5028
|
+
if (rel.startsWith("..") || path22.isAbsolute(rel)) return void 0;
|
|
5029
|
+
const segs = rel.split(path22.sep);
|
|
4558
5030
|
if (segs.length < 2) return void 0;
|
|
4559
5031
|
const feature = segs[0];
|
|
4560
|
-
const file = segs.slice(1).join(
|
|
5032
|
+
const file = segs.slice(1).join(path22.sep);
|
|
4561
5033
|
if (featureFilter && feature !== featureFilter) return void 0;
|
|
4562
|
-
return { feature, file, abs:
|
|
5034
|
+
return { feature, file, abs: path22.resolve(absPath) };
|
|
4563
5035
|
}
|
|
4564
5036
|
function resolvePlanFromArg(arg, paths, featureFilter) {
|
|
4565
5037
|
const t = arg.trim();
|
|
4566
5038
|
if (!t) return void 0;
|
|
4567
|
-
const asPath =
|
|
5039
|
+
const asPath = path22.isAbsolute(t) ? t : path22.resolve(process.cwd(), t);
|
|
4568
5040
|
const a = tryParsePlanFilePath(asPath, paths, featureFilter);
|
|
4569
5041
|
if (a) return a;
|
|
4570
|
-
if (!
|
|
4571
|
-
const under = tryParsePlanFilePath(
|
|
5042
|
+
if (!path22.isAbsolute(t)) {
|
|
5043
|
+
const under = tryParsePlanFilePath(path22.join(paths.plansDir, t), paths, featureFilter);
|
|
4572
5044
|
if (under) return under;
|
|
4573
5045
|
}
|
|
4574
5046
|
if (isSequenceToken(t)) {
|
|
@@ -4598,24 +5070,24 @@ function countOverviewDataRows(overviewContent) {
|
|
|
4598
5070
|
return n;
|
|
4599
5071
|
}
|
|
4600
5072
|
function featureStoriesDirEmpty(paths, feature) {
|
|
4601
|
-
const d =
|
|
4602
|
-
if (!
|
|
4603
|
-
return
|
|
5073
|
+
const d = path22.join(paths.storiesDir, feature);
|
|
5074
|
+
if (!fs28.existsSync(d)) return true;
|
|
5075
|
+
return fs28.readdirSync(d).length === 0;
|
|
4604
5076
|
}
|
|
4605
5077
|
|
|
4606
5078
|
// src/commands/rm/story.ts
|
|
4607
|
-
import
|
|
4608
|
-
import
|
|
5079
|
+
import fs30 from "fs";
|
|
5080
|
+
import path24 from "path";
|
|
4609
5081
|
import { confirm as confirm10 } from "@inquirer/prompts";
|
|
4610
5082
|
|
|
4611
5083
|
// src/commands/rm/feature.ts
|
|
4612
|
-
import
|
|
4613
|
-
import
|
|
5084
|
+
import fs29 from "fs";
|
|
5085
|
+
import path23 from "path";
|
|
4614
5086
|
import { confirm as confirm9 } from "@inquirer/prompts";
|
|
4615
5087
|
function countStoryDirs(storiesDir, feature) {
|
|
4616
|
-
const d =
|
|
4617
|
-
if (!
|
|
4618
|
-
return
|
|
5088
|
+
const d = path23.join(storiesDir, feature);
|
|
5089
|
+
if (!fs29.existsSync(d)) return 0;
|
|
5090
|
+
return fs29.readdirSync(d, { withFileTypes: true }).filter((x) => x.isDirectory()).length;
|
|
4619
5091
|
}
|
|
4620
5092
|
async function runRmFeature(featureSlug, opts = {}) {
|
|
4621
5093
|
const root = requireSquadRoot();
|
|
@@ -4649,17 +5121,17 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
4649
5121
|
feature = await pickFeatureInteractive(paths);
|
|
4650
5122
|
}
|
|
4651
5123
|
}
|
|
4652
|
-
const storyDir =
|
|
4653
|
-
const planDir =
|
|
5124
|
+
const storyDir = path23.join(paths.storiesDir, feature);
|
|
5125
|
+
const planDir = path23.join(paths.plansDir, feature);
|
|
4654
5126
|
const nStories = countStoryDirs(paths.storiesDir, feature);
|
|
4655
5127
|
const nPlans = listAllPlanListEntries(paths, feature).length;
|
|
4656
|
-
const hasOverview =
|
|
5128
|
+
const hasOverview = fs29.existsSync(path23.join(planDir, "00-overview.md")) ? 1 : 0;
|
|
4657
5129
|
const targets = [];
|
|
4658
|
-
if (
|
|
4659
|
-
targets.push({ label:
|
|
5130
|
+
if (fs29.existsSync(storyDir)) {
|
|
5131
|
+
targets.push({ label: path23.relative(root, storyDir), absPath: storyDir, kind: "dir" });
|
|
4660
5132
|
}
|
|
4661
|
-
if (
|
|
4662
|
-
targets.push({ label:
|
|
5133
|
+
if (fs29.existsSync(planDir)) {
|
|
5134
|
+
targets.push({ label: path23.relative(root, planDir), absPath: planDir, kind: "dir" });
|
|
4663
5135
|
}
|
|
4664
5136
|
divider("squad rm feature");
|
|
4665
5137
|
info(`Feature: ${feature}`);
|
|
@@ -4674,7 +5146,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
4674
5146
|
kv(t.kind, t.label, 6);
|
|
4675
5147
|
}
|
|
4676
5148
|
if (useTrash) {
|
|
4677
|
-
info(`Items will move to ${
|
|
5149
|
+
info(`Items will move to ${path23.relative(process.cwd(), paths.trashDir)}/<timestamp>/`);
|
|
4678
5150
|
} else if (!dryRun) {
|
|
4679
5151
|
warning("This is a permanent delete. Pass --trash to move to .squad/.trash/ instead.");
|
|
4680
5152
|
}
|
|
@@ -4707,7 +5179,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
4707
5179
|
useTrash
|
|
4708
5180
|
);
|
|
4709
5181
|
if (result.trashed) {
|
|
4710
|
-
success(`Feature removed. Items moved to ${
|
|
5182
|
+
success(`Feature removed. Items moved to ${path23.relative(root, result.trashed)}.`);
|
|
4711
5183
|
} else {
|
|
4712
5184
|
success("Feature removed (stories and plans directories).");
|
|
4713
5185
|
}
|
|
@@ -4748,11 +5220,11 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
4748
5220
|
}
|
|
4749
5221
|
}
|
|
4750
5222
|
const targets = [
|
|
4751
|
-
{ label:
|
|
5223
|
+
{ label: path24.relative(root, story.storyDir), absPath: story.storyDir, kind: "dir" }
|
|
4752
5224
|
];
|
|
4753
5225
|
if (story.planFile) {
|
|
4754
|
-
const planAbs =
|
|
4755
|
-
targets.push({ label:
|
|
5226
|
+
const planAbs = path24.join(paths.plansDir, story.feature, story.planFile);
|
|
5227
|
+
targets.push({ label: path24.relative(root, planAbs), absPath: planAbs, kind: "file" });
|
|
4756
5228
|
}
|
|
4757
5229
|
renderPreview("story", targets, {
|
|
4758
5230
|
dryRun,
|
|
@@ -4777,16 +5249,16 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
4777
5249
|
}
|
|
4778
5250
|
const absPaths = targets.map((t) => t.absPath);
|
|
4779
5251
|
const result = trashOrDelete(absPaths, paths.trashDir, useTrash);
|
|
4780
|
-
removeOverviewRow(
|
|
5252
|
+
removeOverviewRow(path24.join(paths.plansDir, story.feature), story.id);
|
|
4781
5253
|
if (result.trashed) {
|
|
4782
|
-
success(`Removed (moved to ${
|
|
5254
|
+
success(`Removed (moved to ${path24.relative(root, result.trashed)}).`);
|
|
4783
5255
|
} else {
|
|
4784
5256
|
success("Removed story folder, plan file (if any), and overview row.");
|
|
4785
5257
|
}
|
|
4786
|
-
const featurePlanDir =
|
|
4787
|
-
const overviewPath =
|
|
4788
|
-
if (
|
|
4789
|
-
const body =
|
|
5258
|
+
const featurePlanDir = path24.join(paths.plansDir, story.feature);
|
|
5259
|
+
const overviewPath = path24.join(featurePlanDir, "00-overview.md");
|
|
5260
|
+
if (fs30.existsSync(overviewPath)) {
|
|
5261
|
+
const body = fs30.readFileSync(overviewPath, "utf8");
|
|
4790
5262
|
const onlyShell = countOverviewDataRows(body) === 0;
|
|
4791
5263
|
if (onlyShell && featureStoriesDirEmpty(paths, story.feature)) {
|
|
4792
5264
|
if (canPrompt) {
|
|
@@ -4806,7 +5278,7 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
4806
5278
|
}
|
|
4807
5279
|
|
|
4808
5280
|
// src/commands/rm/plan.ts
|
|
4809
|
-
import
|
|
5281
|
+
import path25 from "path";
|
|
4810
5282
|
import { confirm as confirm11 } from "@inquirer/prompts";
|
|
4811
5283
|
async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
4812
5284
|
const root = requireSquadRoot();
|
|
@@ -4842,7 +5314,7 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
4842
5314
|
}
|
|
4843
5315
|
const storyRows = listStories(paths, { feature: plan.feature });
|
|
4844
5316
|
const linked = storyRows.find((s) => s.planFile === plan.file);
|
|
4845
|
-
const targets = [{ label:
|
|
5317
|
+
const targets = [{ label: path25.relative(root, plan.abs), absPath: plan.abs, kind: "file" }];
|
|
4846
5318
|
renderPreview("plan", targets, {
|
|
4847
5319
|
dryRun,
|
|
4848
5320
|
trash: useTrash,
|
|
@@ -4865,14 +5337,14 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
4865
5337
|
}
|
|
4866
5338
|
}
|
|
4867
5339
|
trashOrDelete([plan.abs], paths.trashDir, useTrash);
|
|
4868
|
-
removeOverviewRowForPlanFile(
|
|
4869
|
-
const intakeHint = linked ?
|
|
5340
|
+
removeOverviewRowForPlanFile(path25.join(paths.plansDir, plan.feature), plan.file);
|
|
5341
|
+
const intakeHint = linked ? path25.relative(root, linked.intakePath) : `under .squad/stories/${plan.feature}/`;
|
|
4870
5342
|
success(`Plan removed. Run \`squad new-plan ${intakeHint}\` to regenerate.`);
|
|
4871
5343
|
}
|
|
4872
5344
|
|
|
4873
5345
|
// src/cli.ts
|
|
4874
5346
|
var program = new Command();
|
|
4875
|
-
program.name("squad").description("Plan once, execute cheap. A 3-step SDD workflow CLI.").version("0.
|
|
5347
|
+
program.name("squad").description("Plan once, execute cheap. A 3-step SDD workflow CLI.").version("0.3.0");
|
|
4876
5348
|
program.command("init").description("Bootstrap .squad/ in the current directory").option("--agents <list>", "Comma-separated agent list: claude-code,cursor,copilot,gemini").option("--tracker <type>", "Tracker type: none|github|linear|jira|azure").option(
|
|
4877
5349
|
"--tracker-workspace <hostOrOrg>",
|
|
4878
5350
|
"Jira host or Azure org (non-interactive default; also written to config for jira/azure)"
|