squad-kit 0.2.1 → 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 +82 -1
- package/README.md +21 -7
- package/dist/cli.js +857 -353
- 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
|
}
|
|
@@ -2437,7 +2540,8 @@ function rateLimitMessage(err) {
|
|
|
2437
2540
|
google: "https://aistudio.google.com/app/plan_information"
|
|
2438
2541
|
};
|
|
2439
2542
|
const headline = err.retryAfterSec ? `${err.provider} rate limit hit \u2014 provider asked us to wait ${err.retryAfterSec}s before retrying.` : `${err.provider} rate limit hit.`;
|
|
2440
|
-
const
|
|
2543
|
+
const cap = err.maxRetrySec ?? 90;
|
|
2544
|
+
const retried = err.retrySkippedReason === "retry_after_too_long" ? `squad-kit did not auto-retry: the provider's ${err.retryAfterSec}s wait is longer than our ${cap}s cap, so retrying would just burn another request inside the same throttle window.` : err.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.";
|
|
2441
2545
|
return [
|
|
2442
2546
|
headline,
|
|
2443
2547
|
retried,
|
|
@@ -2454,109 +2558,16 @@ function rateLimitMessage(err) {
|
|
|
2454
2558
|
].join("\n");
|
|
2455
2559
|
}
|
|
2456
2560
|
|
|
2457
|
-
// src/planner/
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
const
|
|
2462
|
-
const
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
let budgetExhausted = false;
|
|
2466
|
-
let finishedNormally = false;
|
|
2467
|
-
while (iterations < maxIter) {
|
|
2468
|
-
iterations += 1;
|
|
2469
|
-
if (input6.budget.timedOut()) {
|
|
2470
|
-
return { planText: accumulatedText, budgetExhausted, timedOut: true, finishedNormally: false, iterations };
|
|
2471
|
-
}
|
|
2472
|
-
if (input6.budget.overCost()) {
|
|
2473
|
-
return { planText: accumulatedText, budgetExhausted: true, timedOut: false, finishedNormally: false, iterations };
|
|
2474
|
-
}
|
|
2475
|
-
const request = {
|
|
2476
|
-
systemPrompt: input6.systemPrompt,
|
|
2477
|
-
model: input6.model,
|
|
2478
|
-
tools: [READ_FILE_TOOL],
|
|
2479
|
-
turns,
|
|
2480
|
-
apiKey: input6.apiKey,
|
|
2481
|
-
maxOutputTokens: 8192
|
|
2482
|
-
};
|
|
2483
|
-
let response = await input6.provider.send(request);
|
|
2484
|
-
if (response.usage) {
|
|
2485
|
-
input6.budget.recordUsage(response.usage);
|
|
2486
|
-
input6.onUsage?.(response.usage);
|
|
2487
|
-
}
|
|
2488
|
-
let retriedRateLimit = false;
|
|
2489
|
-
if (response.stopReason === "error" && response.errorKind === "rate_limit") {
|
|
2490
|
-
const waitSec = Math.min(response.retryAfterSec ?? 10, MAX_RATE_LIMIT_RETRY_SEC);
|
|
2491
|
-
input6.onRateLimit?.(waitSec);
|
|
2492
|
-
await (input6.sleep ?? defaultSleep)(waitSec * 1e3);
|
|
2493
|
-
response = await input6.provider.send(request);
|
|
2494
|
-
if (response.usage) {
|
|
2495
|
-
input6.budget.recordUsage(response.usage);
|
|
2496
|
-
input6.onUsage?.(response.usage);
|
|
2497
|
-
}
|
|
2498
|
-
retriedRateLimit = true;
|
|
2499
|
-
}
|
|
2500
|
-
if (response.stopReason === "error") {
|
|
2501
|
-
throw composePlannerError(input6.provider.name, response, retriedRateLimit);
|
|
2502
|
-
}
|
|
2503
|
-
if (response.text) {
|
|
2504
|
-
accumulatedText += response.text;
|
|
2505
|
-
input6.onAssistantText?.(response.text);
|
|
2506
|
-
}
|
|
2507
|
-
if (response.stopReason === "max_tokens" && !response.toolCalls?.length) {
|
|
2508
|
-
return { planText: accumulatedText, budgetExhausted, timedOut: false, finishedNormally: false, iterations };
|
|
2509
|
-
}
|
|
2510
|
-
if (response.stopReason === "end_turn" || !response.toolCalls?.length) {
|
|
2511
|
-
finishedNormally = true;
|
|
2512
|
-
return { planText: accumulatedText, budgetExhausted, timedOut: false, finishedNormally, iterations };
|
|
2513
|
-
}
|
|
2514
|
-
turns.push({
|
|
2515
|
-
role: "assistant",
|
|
2516
|
-
text: response.text,
|
|
2517
|
-
toolCalls: response.toolCalls
|
|
2518
|
-
});
|
|
2519
|
-
const toolResults = [];
|
|
2520
|
-
for (const tc of response.toolCalls) {
|
|
2521
|
-
if (tc.name !== READ_FILE_TOOL.name) {
|
|
2522
|
-
toolResults.push({ toolCallId: tc.id, content: `unknown tool "${tc.name}"`, isError: true });
|
|
2523
|
-
continue;
|
|
2524
|
-
}
|
|
2525
|
-
const before = input6.budget.snapshot().bytes;
|
|
2526
|
-
const result = readFileTool(input6.root, input6.budget, tc.input);
|
|
2527
|
-
const after = input6.budget.snapshot().bytes;
|
|
2528
|
-
input6.onToolCall?.(tc, after - before, after);
|
|
2529
|
-
if (result.isError && (/budget/i.test(result.content) || /max file reads/i.test(result.content))) {
|
|
2530
|
-
budgetExhausted = true;
|
|
2531
|
-
}
|
|
2532
|
-
toolResults.push({ toolCallId: tc.id, content: result.content, isError: result.isError });
|
|
2533
|
-
}
|
|
2534
|
-
turns.push({ role: "user", toolResults });
|
|
2535
|
-
if (budgetExhausted) {
|
|
2536
|
-
turns.push({
|
|
2537
|
-
role: "user",
|
|
2538
|
-
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."
|
|
2539
|
-
});
|
|
2540
|
-
}
|
|
2541
|
-
}
|
|
2542
|
-
return { planText: accumulatedText, budgetExhausted, timedOut: false, finishedNormally: false, iterations };
|
|
2543
|
-
}
|
|
2544
|
-
function composePlannerError(providerName, response, retriedRateLimit) {
|
|
2545
|
-
if (response.errorKind === "rate_limit") {
|
|
2546
|
-
return new Error(
|
|
2547
|
-
rateLimitMessage({
|
|
2548
|
-
provider: providerName,
|
|
2549
|
-
retryAfterSec: response.retryAfterSec,
|
|
2550
|
-
rawBody: response.rawError ?? "",
|
|
2551
|
-
retryAlreadyAttempted: retriedRateLimit
|
|
2552
|
-
})
|
|
2553
|
-
);
|
|
2554
|
-
}
|
|
2555
|
-
if (response.errorKind === "model_not_found") {
|
|
2556
|
-
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]);
|
|
2557
2569
|
}
|
|
2558
|
-
|
|
2559
|
-
return new Error(`${base} Run \`squad doctor\` to diagnose, or retry \u2014 most 5xx errors are transient.`);
|
|
2570
|
+
return out;
|
|
2560
2571
|
}
|
|
2561
2572
|
|
|
2562
2573
|
// src/planner/providers/anthropic.ts
|
|
@@ -2568,14 +2579,39 @@ var anthropicProvider = {
|
|
|
2568
2579
|
return callAnthropic(req);
|
|
2569
2580
|
}
|
|
2570
2581
|
};
|
|
2571
|
-
|
|
2572
|
-
|
|
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 = {
|
|
2573
2596
|
model: req.model,
|
|
2574
2597
|
max_tokens: req.maxOutputTokens ?? 4096,
|
|
2575
|
-
system:
|
|
2576
|
-
|
|
2577
|
-
|
|
2598
|
+
system: [
|
|
2599
|
+
{
|
|
2600
|
+
type: "text",
|
|
2601
|
+
text: req.systemPrompt,
|
|
2602
|
+
cache_control: { type: "ephemeral" }
|
|
2603
|
+
}
|
|
2604
|
+
],
|
|
2605
|
+
tools: req.tools.map(toAnthropicTool)
|
|
2578
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 ?? [] };
|
|
2579
2615
|
const res = await fetch(API_URL, {
|
|
2580
2616
|
method: "POST",
|
|
2581
2617
|
headers: {
|
|
@@ -2615,26 +2651,73 @@ async function callAnthropic(req) {
|
|
|
2615
2651
|
stopReason: mapStop(json.stop_reason),
|
|
2616
2652
|
usage: {
|
|
2617
2653
|
inputTokens: json.usage?.input_tokens ?? 0,
|
|
2618
|
-
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
|
|
2619
2657
|
}
|
|
2620
2658
|
};
|
|
2621
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
|
+
}
|
|
2622
2691
|
function toAnthropicTool(t) {
|
|
2623
|
-
return {
|
|
2692
|
+
return {
|
|
2693
|
+
name: t.name,
|
|
2694
|
+
description: t.description,
|
|
2695
|
+
input_schema: sortRecordKeys(t.inputSchema)
|
|
2696
|
+
};
|
|
2624
2697
|
}
|
|
2625
2698
|
function toAnthropicMessage(turn) {
|
|
2626
2699
|
const blocks = [];
|
|
2627
2700
|
if (turn.text) blocks.push({ type: "text", text: turn.text });
|
|
2628
|
-
if (turn.toolCalls)
|
|
2629
|
-
for (const tc of turn.toolCalls)
|
|
2630
|
-
|
|
2631
|
-
|
|
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) {
|
|
2632
2713
|
blocks.push({
|
|
2633
2714
|
type: "tool_result",
|
|
2634
2715
|
tool_use_id: tr.toolCallId,
|
|
2635
2716
|
content: tr.content,
|
|
2636
2717
|
is_error: tr.isError ?? false
|
|
2637
2718
|
});
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2638
2721
|
return { role: turn.role === "assistant" ? "assistant" : "user", content: blocks };
|
|
2639
2722
|
}
|
|
2640
2723
|
function mapStop(r) {
|
|
@@ -2651,15 +2734,23 @@ var openaiProvider = {
|
|
|
2651
2734
|
return callOpenAI(req);
|
|
2652
2735
|
}
|
|
2653
2736
|
};
|
|
2654
|
-
|
|
2655
|
-
const
|
|
2656
|
-
for (const turn of req.turns) messages.push(...toOpenAIMessages(turn));
|
|
2657
|
-
const body = {
|
|
2737
|
+
function buildOpenAIBody(req) {
|
|
2738
|
+
const base = {
|
|
2658
2739
|
model: req.model,
|
|
2659
|
-
|
|
2660
|
-
tools: req.tools.map(toOpenAITool)
|
|
2661
|
-
max_completion_tokens: req.maxOutputTokens ?? 4096
|
|
2740
|
+
max_completion_tokens: req.maxOutputTokens ?? 4096,
|
|
2741
|
+
tools: req.tools.map(toOpenAITool)
|
|
2662
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 };
|
|
2663
2754
|
const res = await fetch(API_URL2, {
|
|
2664
2755
|
method: "POST",
|
|
2665
2756
|
headers: {
|
|
@@ -2709,7 +2800,14 @@ async function callOpenAI(req) {
|
|
|
2709
2800
|
};
|
|
2710
2801
|
}
|
|
2711
2802
|
function toOpenAITool(t) {
|
|
2712
|
-
return {
|
|
2803
|
+
return {
|
|
2804
|
+
type: "function",
|
|
2805
|
+
function: {
|
|
2806
|
+
name: t.name,
|
|
2807
|
+
description: t.description,
|
|
2808
|
+
parameters: sortRecordKeys(t.inputSchema)
|
|
2809
|
+
}
|
|
2810
|
+
};
|
|
2713
2811
|
}
|
|
2714
2812
|
function toOpenAIMessages(turn) {
|
|
2715
2813
|
const out = [];
|
|
@@ -2720,7 +2818,10 @@ function toOpenAIMessages(turn) {
|
|
|
2720
2818
|
tool_calls: turn.toolCalls?.map((tc) => ({
|
|
2721
2819
|
id: tc.id,
|
|
2722
2820
|
type: "function",
|
|
2723
|
-
function: {
|
|
2821
|
+
function: {
|
|
2822
|
+
name: tc.name,
|
|
2823
|
+
arguments: JSON.stringify(sortRecordKeys(tc.input))
|
|
2824
|
+
}
|
|
2724
2825
|
}))
|
|
2725
2826
|
});
|
|
2726
2827
|
} else {
|
|
@@ -2740,7 +2841,7 @@ function mapStop2(r) {
|
|
|
2740
2841
|
}
|
|
2741
2842
|
function safeJson(raw) {
|
|
2742
2843
|
try {
|
|
2743
|
-
return JSON.parse(raw);
|
|
2844
|
+
return sortRecordKeys(JSON.parse(raw));
|
|
2744
2845
|
} catch {
|
|
2745
2846
|
return {};
|
|
2746
2847
|
}
|
|
@@ -2754,14 +2855,23 @@ var googleProvider = {
|
|
|
2754
2855
|
return callGoogle(req);
|
|
2755
2856
|
}
|
|
2756
2857
|
};
|
|
2757
|
-
|
|
2758
|
-
const
|
|
2759
|
-
const body = {
|
|
2858
|
+
function buildGoogleBody(req) {
|
|
2859
|
+
const base = {
|
|
2760
2860
|
system_instruction: { parts: [{ text: req.systemPrompt }] },
|
|
2761
|
-
contents: req.turns.map(toGoogleContent),
|
|
2762
|
-
tools: req.tools.length ? [{ functionDeclarations: req.tools.map(toGoogleTool) }] : void 0,
|
|
2763
2861
|
generationConfig: { maxOutputTokens: req.maxOutputTokens ?? 4096 }
|
|
2764
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 ?? [] };
|
|
2765
2875
|
const res = await fetch(url, {
|
|
2766
2876
|
method: "POST",
|
|
2767
2877
|
headers: { "content-type": "application/json" },
|
|
@@ -2796,7 +2906,7 @@ async function callGoogle(req) {
|
|
|
2796
2906
|
const toolCalls = parts.filter((p) => "functionCall" in p && !!p.functionCall).map((p, i) => ({
|
|
2797
2907
|
id: `gcall_${i}_${p.functionCall.name}`,
|
|
2798
2908
|
name: p.functionCall.name,
|
|
2799
|
-
input: p.functionCall.args
|
|
2909
|
+
input: p.functionCall.args ? sortRecordKeys(p.functionCall.args) : {}
|
|
2800
2910
|
}));
|
|
2801
2911
|
return {
|
|
2802
2912
|
text: textOut || void 0,
|
|
@@ -2809,14 +2919,17 @@ async function callGoogle(req) {
|
|
|
2809
2919
|
};
|
|
2810
2920
|
}
|
|
2811
2921
|
function toGoogleTool(t) {
|
|
2812
|
-
return { name: t.name, description: t.description, parameters: t.inputSchema };
|
|
2922
|
+
return { name: t.name, description: t.description, parameters: sortRecordKeys(t.inputSchema) };
|
|
2813
2923
|
}
|
|
2814
2924
|
function toGoogleContent(turn) {
|
|
2815
2925
|
const role = turn.role === "assistant" ? "model" : "user";
|
|
2816
2926
|
const parts = [];
|
|
2817
2927
|
if (turn.text) parts.push({ text: turn.text });
|
|
2818
|
-
if (turn.toolCalls)
|
|
2819
|
-
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
|
+
}
|
|
2820
2933
|
if (turn.toolResults) {
|
|
2821
2934
|
for (const tr of turn.toolResults) {
|
|
2822
2935
|
parts.push({
|
|
@@ -2836,6 +2949,208 @@ function mapStop3(r) {
|
|
|
2836
2949
|
return "tool_use";
|
|
2837
2950
|
}
|
|
2838
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
|
+
|
|
2839
3154
|
// src/planner/providers/index.ts
|
|
2840
3155
|
function providerFor(name) {
|
|
2841
3156
|
switch (name) {
|
|
@@ -2857,7 +3172,7 @@ var Budget = class {
|
|
|
2857
3172
|
reads = 0;
|
|
2858
3173
|
bytes = 0;
|
|
2859
3174
|
startedAt = Date.now();
|
|
2860
|
-
totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
3175
|
+
totalUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
|
|
2861
3176
|
canRead(nextFileBytes) {
|
|
2862
3177
|
if (this.reads >= this.cfg.maxFileReads) {
|
|
2863
3178
|
return { ok: false, reason: `max file reads (${this.cfg.maxFileReads}) reached` };
|
|
@@ -2875,6 +3190,8 @@ var Budget = class {
|
|
|
2875
3190
|
this.totalUsage.inputTokens += u.inputTokens;
|
|
2876
3191
|
this.totalUsage.outputTokens += u.outputTokens;
|
|
2877
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);
|
|
2878
3195
|
}
|
|
2879
3196
|
timedOut() {
|
|
2880
3197
|
return (Date.now() - this.startedAt) / 1e3 > this.cfg.maxDurationSeconds;
|
|
@@ -3020,8 +3337,8 @@ function formatSequence(n) {
|
|
|
3020
3337
|
}
|
|
3021
3338
|
|
|
3022
3339
|
// src/planner/writer.ts
|
|
3023
|
-
function writePlanFile(
|
|
3024
|
-
const { paths, config: config2, story } =
|
|
3340
|
+
function writePlanFile(input5) {
|
|
3341
|
+
const { paths, config: config2, story } = input5;
|
|
3025
3342
|
const featurePlanDir = path13.join(paths.plansDir, story.feature);
|
|
3026
3343
|
fs20.mkdirSync(featurePlanDir, { recursive: true });
|
|
3027
3344
|
const existing = findExistingPlan(featurePlanDir, story.id);
|
|
@@ -3040,9 +3357,9 @@ function writePlanFile(input6) {
|
|
|
3040
3357
|
const trackerPart = trackerId && trackerId !== baseSlug ? `-${trackerId}` : "";
|
|
3041
3358
|
targetPath = path13.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.md`);
|
|
3042
3359
|
}
|
|
3043
|
-
const body = `${
|
|
3360
|
+
const body = `${input5.metadataHeader.trimEnd()}
|
|
3044
3361
|
|
|
3045
|
-
${
|
|
3362
|
+
${input5.planBodyMarkdown.trimStart()}
|
|
3046
3363
|
`;
|
|
3047
3364
|
fs20.writeFileSync(targetPath, body, "utf8");
|
|
3048
3365
|
upsertOverviewRow(featurePlanDir, story.feature, nn, path13.basename(targetPath), story);
|
|
@@ -3109,6 +3426,45 @@ function buildMetadataHeader(args) {
|
|
|
3109
3426
|
return `<!-- squad-kit: ${parts.join(", ")} -->`;
|
|
3110
3427
|
}
|
|
3111
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
|
+
|
|
3112
3468
|
// src/commands/new-plan.ts
|
|
3113
3469
|
function decideMode(opts, config2) {
|
|
3114
3470
|
if (opts.api && opts.copy) {
|
|
@@ -3140,8 +3496,8 @@ async function runNewPlan(intakePath, opts) {
|
|
|
3140
3496
|
}
|
|
3141
3497
|
}
|
|
3142
3498
|
function resolveFromPath(intakePath, stories, root) {
|
|
3143
|
-
const resolved =
|
|
3144
|
-
if (!
|
|
3499
|
+
const resolved = path15.resolve(intakePath.trim());
|
|
3500
|
+
if (!fs22.existsSync(resolved)) {
|
|
3145
3501
|
throw new Error(
|
|
3146
3502
|
`Intake not found: ${resolved}. Run \`squad list\` to see intakes, or \`squad new-story\` to create one first.`
|
|
3147
3503
|
);
|
|
@@ -3153,7 +3509,7 @@ function resolveFromPath(intakePath, stories, root) {
|
|
|
3153
3509
|
);
|
|
3154
3510
|
}
|
|
3155
3511
|
function relativeStoriesDir(root) {
|
|
3156
|
-
return
|
|
3512
|
+
return path15.relative(root, path15.join(root, ".squad", "stories"));
|
|
3157
3513
|
}
|
|
3158
3514
|
async function pickStory(stories, opts) {
|
|
3159
3515
|
const candidates = opts.all ? stories : stories.filter((s) => !s.planFile);
|
|
@@ -3184,7 +3540,7 @@ async function pickStory(stories, opts) {
|
|
|
3184
3540
|
value: s.intakePath
|
|
3185
3541
|
}))
|
|
3186
3542
|
});
|
|
3187
|
-
return candidates.find((s) =>
|
|
3543
|
+
return candidates.find((s) => path15.resolve(s.intakePath) === path15.resolve(pick));
|
|
3188
3544
|
}
|
|
3189
3545
|
function formatLabel(s) {
|
|
3190
3546
|
const head = `${s.feature} / ${s.id}`;
|
|
@@ -3245,6 +3601,7 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3245
3601
|
const userPrompt = composeUserPrompt({ intakeContent: readFile(story.intakePath) });
|
|
3246
3602
|
const sessionSpinner = { current: null };
|
|
3247
3603
|
const startedAt = Date.now();
|
|
3604
|
+
const cacheEnabled = planner.cache?.enabled ?? true;
|
|
3248
3605
|
const result = await runPlanner({
|
|
3249
3606
|
root: paths.root,
|
|
3250
3607
|
provider,
|
|
@@ -3253,6 +3610,7 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3253
3610
|
systemPrompt,
|
|
3254
3611
|
userPrompt,
|
|
3255
3612
|
budget,
|
|
3613
|
+
cacheEnabled,
|
|
3256
3614
|
onToolCall: (tc, bytes, total) => {
|
|
3257
3615
|
const p = tc.input.path ?? "<unknown>";
|
|
3258
3616
|
sessionSpinner.current?.succeed(
|
|
@@ -3295,9 +3653,18 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3295
3653
|
planBodyMarkdown: result.planText,
|
|
3296
3654
|
metadataHeader: header
|
|
3297
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
|
+
}
|
|
3298
3665
|
blank();
|
|
3299
3666
|
summaryBox(" plan generated ", [
|
|
3300
|
-
{ key: "file", value:
|
|
3667
|
+
{ key: "file", value: path15.relative(paths.root, planFile) },
|
|
3301
3668
|
{ key: "nn", value: String(sequenceNumber).padStart(2, "0") },
|
|
3302
3669
|
{ key: "model", value: `${planner.provider}/${model}` },
|
|
3303
3670
|
{ key: "reads", value: `${snap.reads} files \xB7 ${(snap.bytes / 1024).toFixed(1)} KB` },
|
|
@@ -3305,6 +3672,7 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3305
3672
|
key: "tokens",
|
|
3306
3673
|
value: `${snap.usage.inputTokens} in \xB7 ${snap.usage.outputTokens} out${snap.usage.costUsd !== void 0 ? ` \u2248 $${snap.usage.costUsd.toFixed(2)}` : ""}`
|
|
3307
3674
|
},
|
|
3675
|
+
{ key: "cache", value: formatPlannerCacheLine({ cacheEnabled, stats: result.stats }) },
|
|
3308
3676
|
{ key: "time", value: `${Math.round(elapsedMs / 1e3)}s` },
|
|
3309
3677
|
{ key: "action", value: overwrote ? "overwrote existing plan" : "new plan" }
|
|
3310
3678
|
]);
|
|
@@ -3314,9 +3682,9 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3314
3682
|
warning("Planner did not reach end_turn. Inspect the plan carefully.");
|
|
3315
3683
|
}
|
|
3316
3684
|
blank();
|
|
3317
|
-
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)}`);
|
|
3318
3686
|
}
|
|
3319
|
-
async function emitCopyPrompt(story,
|
|
3687
|
+
async function emitCopyPrompt(story, paths, config2, clipboard) {
|
|
3320
3688
|
const intakeContent = readFile(story.intakePath);
|
|
3321
3689
|
const metaPromptTemplate = readBundledPrompt("generate-plan.md");
|
|
3322
3690
|
const composed = render(metaPromptTemplate, {
|
|
@@ -3325,13 +3693,44 @@ async function emitCopyPrompt(story, _paths, config2, clipboard) {
|
|
|
3325
3693
|
trackerType: config2.tracker.type,
|
|
3326
3694
|
intakeContent
|
|
3327
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
|
+
}
|
|
3328
3713
|
printModelBanner(config2);
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
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.`);
|
|
3334
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`;
|
|
3335
3734
|
}
|
|
3336
3735
|
function printModelBanner(config2) {
|
|
3337
3736
|
if (!isInteractive()) return;
|
|
@@ -3354,7 +3753,7 @@ function printModelBanner(config2) {
|
|
|
3354
3753
|
}
|
|
3355
3754
|
|
|
3356
3755
|
// src/commands/status.ts
|
|
3357
|
-
import
|
|
3756
|
+
import fs23 from "fs";
|
|
3358
3757
|
var kw = 28;
|
|
3359
3758
|
function probeTrackerCredential(config2, overlay) {
|
|
3360
3759
|
const { client, error: error2 } = clientFor(config2, overlay);
|
|
@@ -3380,7 +3779,7 @@ async function runStatus() {
|
|
|
3380
3779
|
const scan = scanPlans(paths.plansDir);
|
|
3381
3780
|
const storyCount = listStories(paths).length;
|
|
3382
3781
|
const planCount = scan.usedNumbers.length;
|
|
3383
|
-
const secrets =
|
|
3782
|
+
const secrets = fs23.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
3384
3783
|
const overlay = overlayTrackerEnv(secrets);
|
|
3385
3784
|
banner();
|
|
3386
3785
|
step("squad-kit status");
|
|
@@ -3434,13 +3833,13 @@ function buildTrackerLine(config2, overlay) {
|
|
|
3434
3833
|
}
|
|
3435
3834
|
|
|
3436
3835
|
// src/commands/list.ts
|
|
3437
|
-
import
|
|
3836
|
+
import path16 from "path";
|
|
3438
3837
|
async function runList(opts) {
|
|
3439
3838
|
const root = requireSquadRoot();
|
|
3440
3839
|
const paths = buildPaths(root);
|
|
3441
3840
|
const records = listStories(paths, { feature: opts.feature });
|
|
3442
3841
|
const rows = records.map((r) => {
|
|
3443
|
-
const planAbs = r.planFile ?
|
|
3842
|
+
const planAbs = r.planFile ? path16.join(paths.plansDir, r.feature, r.planFile) : void 0;
|
|
3444
3843
|
const meta = planAbs ? readPlanMetadata(planAbs) : void 0;
|
|
3445
3844
|
return {
|
|
3446
3845
|
feature: r.feature,
|
|
@@ -3479,9 +3878,9 @@ function stripAnsi(s) {
|
|
|
3479
3878
|
}
|
|
3480
3879
|
|
|
3481
3880
|
// src/commands/tracker-link.ts
|
|
3482
|
-
import
|
|
3483
|
-
import
|
|
3484
|
-
import { input as
|
|
3881
|
+
import fs24 from "fs";
|
|
3882
|
+
import path17 from "path";
|
|
3883
|
+
import { input as input4, select as select6 } from "@inquirer/prompts";
|
|
3485
3884
|
async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
3486
3885
|
const root = requireSquadRoot();
|
|
3487
3886
|
const paths = buildPaths(root);
|
|
@@ -3513,7 +3912,7 @@ async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
|
3513
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."
|
|
3514
3913
|
);
|
|
3515
3914
|
}
|
|
3516
|
-
resolvedTrackerId = (await
|
|
3915
|
+
resolvedTrackerId = (await input4({
|
|
3517
3916
|
message: `${config2.tracker.type} work-item id:`,
|
|
3518
3917
|
validate: (v) => {
|
|
3519
3918
|
const trimmed = v.trim();
|
|
@@ -3527,17 +3926,21 @@ async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
|
3527
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.`
|
|
3528
3927
|
);
|
|
3529
3928
|
}
|
|
3530
|
-
const resolved =
|
|
3531
|
-
const intakeFile = resolved.endsWith("intake.md") ? resolved :
|
|
3532
|
-
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)) {
|
|
3533
3932
|
throw new Error(
|
|
3534
3933
|
`intake.md not found at ${intakeFile}. Run \`squad list\` to see valid paths, or \`squad new-story\` to create a story first.`
|
|
3535
3934
|
);
|
|
3536
3935
|
}
|
|
3537
|
-
const content =
|
|
3936
|
+
const content = fs24.readFileSync(intakeFile, "utf8");
|
|
3538
3937
|
const updated = upsertTrackerId(content, resolvedTrackerId);
|
|
3539
|
-
|
|
3540
|
-
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.");
|
|
3541
3944
|
}
|
|
3542
3945
|
function upsertTrackerId(content, id) {
|
|
3543
3946
|
const pattern = /(-\s*\*\*Work item id:\*\*)[^\n]*/;
|
|
@@ -3551,8 +3954,8 @@ function upsertTrackerId(content, id) {
|
|
|
3551
3954
|
}
|
|
3552
3955
|
|
|
3553
3956
|
// src/commands/doctor.ts
|
|
3554
|
-
import
|
|
3555
|
-
import
|
|
3957
|
+
import fs25 from "fs";
|
|
3958
|
+
import path18 from "path";
|
|
3556
3959
|
async function gatherContext(paths) {
|
|
3557
3960
|
const ctx = { hasLegacyPromptsDir: false };
|
|
3558
3961
|
try {
|
|
@@ -3561,29 +3964,29 @@ async function gatherContext(paths) {
|
|
|
3561
3964
|
ctx.configError = err;
|
|
3562
3965
|
}
|
|
3563
3966
|
try {
|
|
3564
|
-
ctx.secrets =
|
|
3967
|
+
ctx.secrets = fs25.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
3565
3968
|
} catch (err) {
|
|
3566
3969
|
ctx.secretsError = err;
|
|
3567
3970
|
}
|
|
3568
|
-
ctx.hasLegacyPromptsDir =
|
|
3971
|
+
ctx.hasLegacyPromptsDir = fs25.existsSync(paths.promptsDir);
|
|
3569
3972
|
return ctx;
|
|
3570
3973
|
}
|
|
3571
3974
|
function gitignoreHasManagedBlock(repoRoot) {
|
|
3572
|
-
const gitignore =
|
|
3573
|
-
return
|
|
3975
|
+
const gitignore = path18.join(repoRoot, ".gitignore");
|
|
3976
|
+
return fs25.existsSync(gitignore) && fs25.readFileSync(gitignore, "utf8").includes(".squad/secrets.yaml");
|
|
3574
3977
|
}
|
|
3575
3978
|
function gitignoreHasTrashPattern(repoRoot) {
|
|
3576
|
-
const gitignore =
|
|
3577
|
-
return
|
|
3979
|
+
const gitignore = path18.join(repoRoot, ".gitignore");
|
|
3980
|
+
return fs25.existsSync(gitignore) && fs25.readFileSync(gitignore, "utf8").includes(SQUAD_TRASH_PATTERN);
|
|
3578
3981
|
}
|
|
3579
3982
|
async function checkDirStructure(paths, _ctx, fix) {
|
|
3580
|
-
const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !
|
|
3983
|
+
const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !fs25.existsSync(p));
|
|
3581
3984
|
if (need.length === 0) {
|
|
3582
3985
|
return { id: "dirs", name: ".squad/ directory structure", status: "ok" };
|
|
3583
3986
|
}
|
|
3584
3987
|
if (fix) {
|
|
3585
3988
|
for (const p of need) {
|
|
3586
|
-
|
|
3989
|
+
fs25.mkdirSync(p, { recursive: true });
|
|
3587
3990
|
}
|
|
3588
3991
|
return { id: "dirs", name: ".squad/ directory structure", status: "ok", detail: "repaired" };
|
|
3589
3992
|
}
|
|
@@ -3591,7 +3994,7 @@ async function checkDirStructure(paths, _ctx, fix) {
|
|
|
3591
3994
|
id: "dirs",
|
|
3592
3995
|
name: ".squad/ directory structure",
|
|
3593
3996
|
status: "warn",
|
|
3594
|
-
detail: `missing: ${need.map((p) =>
|
|
3997
|
+
detail: `missing: ${need.map((p) => path18.relative(paths.root, p)).join(", ")}`,
|
|
3595
3998
|
fixable: true,
|
|
3596
3999
|
fixHint: "squad doctor --fix"
|
|
3597
4000
|
};
|
|
@@ -3646,15 +4049,15 @@ async function checkSecretsPermissions(paths, _ctx, fix) {
|
|
|
3646
4049
|
if (process.platform === "win32") {
|
|
3647
4050
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "skip", detail: "Windows" };
|
|
3648
4051
|
}
|
|
3649
|
-
if (!
|
|
4052
|
+
if (!fs25.existsSync(paths.secretsFile)) {
|
|
3650
4053
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "not present" };
|
|
3651
4054
|
}
|
|
3652
|
-
const mode =
|
|
4055
|
+
const mode = fs25.statSync(paths.secretsFile).mode & 511;
|
|
3653
4056
|
if (mode === 384) {
|
|
3654
4057
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok" };
|
|
3655
4058
|
}
|
|
3656
4059
|
if (fix) {
|
|
3657
|
-
|
|
4060
|
+
fs25.chmodSync(paths.secretsFile, 384);
|
|
3658
4061
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "repaired" };
|
|
3659
4062
|
}
|
|
3660
4063
|
return {
|
|
@@ -3675,7 +4078,7 @@ async function checkSecretsParseable(_paths, ctx) {
|
|
|
3675
4078
|
detail: ctx.secretsError.message
|
|
3676
4079
|
};
|
|
3677
4080
|
}
|
|
3678
|
-
if (!
|
|
4081
|
+
if (!fs25.existsSync(_paths.secretsFile)) {
|
|
3679
4082
|
return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok", detail: "not present" };
|
|
3680
4083
|
}
|
|
3681
4084
|
return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok" };
|
|
@@ -3826,6 +4229,101 @@ async function checkPlannerModel(_paths, ctx) {
|
|
|
3826
4229
|
};
|
|
3827
4230
|
}
|
|
3828
4231
|
}
|
|
4232
|
+
async function checkPlannerTierAwareness(_paths, ctx) {
|
|
4233
|
+
const id = "planner-tier";
|
|
4234
|
+
const name = "planner tier vs. model";
|
|
4235
|
+
if (!ctx.config || ctx.config.planner?.enabled !== true) {
|
|
4236
|
+
return { id, name, status: "skip", detail: "planner disabled" };
|
|
4237
|
+
}
|
|
4238
|
+
const provider = ctx.config.planner.provider;
|
|
4239
|
+
if (provider !== "anthropic") {
|
|
4240
|
+
return { id, name, status: "skip", detail: `not applicable for ${provider}` };
|
|
4241
|
+
}
|
|
4242
|
+
const planModel = modelFor(provider, "plan", ctx.config.planner.modelOverride);
|
|
4243
|
+
if (!/opus/i.test(planModel)) {
|
|
4244
|
+
return { id, name, status: "ok", detail: `${planModel} is comfortably under Tier 1` };
|
|
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
|
+
}
|
|
4259
|
+
return {
|
|
4260
|
+
id,
|
|
4261
|
+
name,
|
|
4262
|
+
status: "warn",
|
|
4263
|
+
detail: `${planModel} on Anthropic Tier 1 (10k input tokens/min) is throttle-prone for plans over ~3 file reads`,
|
|
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."
|
|
4265
|
+
};
|
|
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
|
+
}
|
|
3829
4327
|
async function checkTrackerConfig(_paths, ctx) {
|
|
3830
4328
|
if (!ctx.config) {
|
|
3831
4329
|
return { id: "tracker-config", name: "tracker configuration", status: "skip", detail: "config unavailable" };
|
|
@@ -3947,6 +4445,8 @@ async function runAllChecks(paths, ctx, fix) {
|
|
|
3947
4445
|
await add("planner configuration", () => checkPlannerConfig(paths, ctx));
|
|
3948
4446
|
await add("planner credential resolves", () => checkPlannerCredential(paths, ctx));
|
|
3949
4447
|
await add("planner model resolves at provider", () => checkPlannerModel(paths, ctx));
|
|
4448
|
+
await add("planner tier vs. model", () => checkPlannerTierAwareness(paths, ctx));
|
|
4449
|
+
await add("planner cache effectiveness", () => checkPlannerCache(paths, ctx));
|
|
3950
4450
|
await add("tracker configuration", () => checkTrackerConfig(paths, ctx));
|
|
3951
4451
|
await add("tracker credential resolves", () => checkTrackerCredential(paths, ctx));
|
|
3952
4452
|
await add("tracker connectivity", () => checkTrackerConnectivity(paths, ctx));
|
|
@@ -4052,23 +4552,23 @@ async function runDoctor(opts) {
|
|
|
4052
4552
|
}
|
|
4053
4553
|
|
|
4054
4554
|
// src/commands/migrate.ts
|
|
4055
|
-
import
|
|
4056
|
-
import
|
|
4555
|
+
import fs26 from "fs";
|
|
4556
|
+
import path19 from "path";
|
|
4057
4557
|
import { confirm as confirm7 } from "@inquirer/prompts";
|
|
4058
4558
|
var LEGACY_PROMPTS_MIGRATION = {
|
|
4059
4559
|
id: "legacy-prompts",
|
|
4060
4560
|
name: "Remove legacy .squad/prompts/",
|
|
4061
|
-
isApplied: (paths) => !
|
|
4062
|
-
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)`,
|
|
4063
4563
|
apply: (paths) => {
|
|
4064
4564
|
const target = paths.promptsDir;
|
|
4065
|
-
if (!target.includes(
|
|
4565
|
+
if (!target.includes(path19.sep + ".squad" + path19.sep + "prompts")) {
|
|
4066
4566
|
throw new Error(
|
|
4067
4567
|
`Refusing to delete suspicious path: ${target}. This is a safety check \u2014 run \`squad doctor\` if .squad/ paths look wrong.`
|
|
4068
4568
|
);
|
|
4069
4569
|
}
|
|
4070
|
-
if (
|
|
4071
|
-
|
|
4570
|
+
if (fs26.existsSync(target)) {
|
|
4571
|
+
fs26.rmSync(target, { recursive: true, force: true });
|
|
4072
4572
|
}
|
|
4073
4573
|
}
|
|
4074
4574
|
};
|
|
@@ -4076,8 +4576,8 @@ var GITIGNORE_MIGRATION = {
|
|
|
4076
4576
|
id: "gitignore",
|
|
4077
4577
|
name: "Ensure .gitignore managed block",
|
|
4078
4578
|
isApplied: (paths) => {
|
|
4079
|
-
const f =
|
|
4080
|
-
return
|
|
4579
|
+
const f = path19.join(paths.root, ".gitignore");
|
|
4580
|
+
return fs26.existsSync(f) && fs26.readFileSync(f, "utf8").includes(".squad/secrets.yaml");
|
|
4081
4581
|
},
|
|
4082
4582
|
describe: () => "add squad-kit managed block to .gitignore",
|
|
4083
4583
|
apply: (paths) => {
|
|
@@ -4089,23 +4589,23 @@ var SECRETS_PERMS_MIGRATION = {
|
|
|
4089
4589
|
name: "Tighten .squad/secrets.yaml permissions",
|
|
4090
4590
|
isApplied: (paths) => {
|
|
4091
4591
|
if (process.platform === "win32") return true;
|
|
4092
|
-
if (!
|
|
4093
|
-
const st =
|
|
4592
|
+
if (!fs26.existsSync(paths.secretsFile)) return true;
|
|
4593
|
+
const st = fs26.statSync(paths.secretsFile);
|
|
4094
4594
|
return (st.mode & 511) === 384;
|
|
4095
4595
|
},
|
|
4096
|
-
describe: (paths) => `chmod 0600 ${
|
|
4596
|
+
describe: (paths) => `chmod 0600 ${path19.relative(paths.root, paths.secretsFile)}`,
|
|
4097
4597
|
apply: (paths) => {
|
|
4098
4598
|
if (process.platform === "win32") return;
|
|
4099
|
-
if (
|
|
4599
|
+
if (fs26.existsSync(paths.secretsFile)) fs26.chmodSync(paths.secretsFile, 384);
|
|
4100
4600
|
}
|
|
4101
4601
|
};
|
|
4102
4602
|
var PLANNER_BUDGET_DEFAULTS_MIGRATION = {
|
|
4103
4603
|
id: "planner-budget-defaults",
|
|
4104
4604
|
name: "Backfill planner.budget defaults in config.yaml",
|
|
4105
4605
|
isApplied: (paths) => {
|
|
4106
|
-
if (!
|
|
4606
|
+
if (!fs26.existsSync(paths.configFile)) return true;
|
|
4107
4607
|
try {
|
|
4108
|
-
const raw =
|
|
4608
|
+
const raw = fs26.readFileSync(paths.configFile, "utf8");
|
|
4109
4609
|
if (!/^\s*planner:/m.test(raw)) return true;
|
|
4110
4610
|
if (/^\s*budget:/m.test(raw)) return true;
|
|
4111
4611
|
const cfg = loadConfig(paths.configFile);
|
|
@@ -4124,9 +4624,9 @@ var CONFIG_NORMALIZE_MIGRATION = {
|
|
|
4124
4624
|
id: "config-normalize",
|
|
4125
4625
|
name: "Normalise .squad/config.yaml formatting",
|
|
4126
4626
|
isApplied: (paths) => {
|
|
4127
|
-
if (!
|
|
4627
|
+
if (!fs26.existsSync(paths.configFile)) return true;
|
|
4128
4628
|
try {
|
|
4129
|
-
const raw =
|
|
4629
|
+
const raw = fs26.readFileSync(paths.configFile, "utf8");
|
|
4130
4630
|
const cfg = loadConfig(paths.configFile);
|
|
4131
4631
|
return raw.trim() === serializeConfig(cfg).trim();
|
|
4132
4632
|
} catch {
|
|
@@ -4135,7 +4635,7 @@ var CONFIG_NORMALIZE_MIGRATION = {
|
|
|
4135
4635
|
},
|
|
4136
4636
|
describe: () => "rewrite config.yaml with canonical YAML formatting (drops comments)",
|
|
4137
4637
|
apply: (paths) => {
|
|
4138
|
-
if (!
|
|
4638
|
+
if (!fs26.existsSync(paths.configFile)) return;
|
|
4139
4639
|
const cfg = loadConfig(paths.configFile);
|
|
4140
4640
|
saveConfig(paths.configFile, cfg);
|
|
4141
4641
|
}
|
|
@@ -4203,7 +4703,11 @@ async function runMigrate(opts) {
|
|
|
4203
4703
|
}
|
|
4204
4704
|
info("");
|
|
4205
4705
|
success(`Applied ${applied}/${pending.length} migration(s).`);
|
|
4206
|
-
|
|
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`.");
|
|
4207
4711
|
}
|
|
4208
4712
|
|
|
4209
4713
|
// src/commands/upgrade.ts
|
|
@@ -4212,8 +4716,8 @@ import process3 from "process";
|
|
|
4212
4716
|
import { confirm as confirm8 } from "@inquirer/prompts";
|
|
4213
4717
|
|
|
4214
4718
|
// src/core/package-info.ts
|
|
4215
|
-
import
|
|
4216
|
-
import
|
|
4719
|
+
import fs27 from "fs";
|
|
4720
|
+
import path20 from "path";
|
|
4217
4721
|
function changelogBlobUrl(repositoryUrl, version) {
|
|
4218
4722
|
let base = repositoryUrl;
|
|
4219
4723
|
if (!base) base = "https://github.com/AzmSquad/squad-kit";
|
|
@@ -4224,9 +4728,9 @@ function changelogBlobUrl(repositoryUrl, version) {
|
|
|
4224
4728
|
}
|
|
4225
4729
|
function readInstalledPackage() {
|
|
4226
4730
|
const root = packageRoot();
|
|
4227
|
-
const pkgFile =
|
|
4228
|
-
const pkg = JSON.parse(
|
|
4229
|
-
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"));
|
|
4230
4734
|
return {
|
|
4231
4735
|
name: pkg.name,
|
|
4232
4736
|
version: pkg.version,
|
|
@@ -4266,7 +4770,7 @@ function isNewer(a, b) {
|
|
|
4266
4770
|
}
|
|
4267
4771
|
|
|
4268
4772
|
// src/core/package-manager.ts
|
|
4269
|
-
import
|
|
4773
|
+
import path21 from "path";
|
|
4270
4774
|
import process2 from "process";
|
|
4271
4775
|
function detectPackageManager(installRoot) {
|
|
4272
4776
|
const ua = process2.env.npm_config_user_agent;
|
|
@@ -4277,11 +4781,11 @@ function detectPackageManager(installRoot) {
|
|
|
4277
4781
|
if (ua.startsWith("npm/")) return { pm: "npm", reason: "npm_config_user_agent" };
|
|
4278
4782
|
}
|
|
4279
4783
|
const root = installRoot.toLowerCase();
|
|
4280
|
-
if (root.includes(`${
|
|
4784
|
+
if (root.includes(`${path21.sep}pnpm${path21.sep}`) || root.includes(`${path21.sep}.pnpm${path21.sep}`))
|
|
4281
4785
|
return { pm: "pnpm", reason: `install path contains pnpm` };
|
|
4282
|
-
if (root.includes(`${
|
|
4786
|
+
if (root.includes(`${path21.sep}.yarn${path21.sep}`) || root.includes(`${path21.sep}yarn${path21.sep}`))
|
|
4283
4787
|
return { pm: "yarn", reason: `install path contains yarn` };
|
|
4284
|
-
if (root.includes(`${
|
|
4788
|
+
if (root.includes(`${path21.sep}.bun${path21.sep}`) || root.includes(`${path21.sep}bun${path21.sep}`))
|
|
4285
4789
|
return { pm: "bun", reason: `install path contains bun` };
|
|
4286
4790
|
return { pm: "npm", reason: "default fallback" };
|
|
4287
4791
|
}
|
|
@@ -4382,8 +4886,8 @@ function spawnInherit(cmd, args) {
|
|
|
4382
4886
|
}
|
|
4383
4887
|
|
|
4384
4888
|
// src/commands/rm/shared.ts
|
|
4385
|
-
import
|
|
4386
|
-
import
|
|
4889
|
+
import fs28 from "fs";
|
|
4890
|
+
import path22 from "path";
|
|
4387
4891
|
import { select as select7 } from "@inquirer/prompts";
|
|
4388
4892
|
var STORY_PLAN = /^(\d{2,})-story-.+\.md$/;
|
|
4389
4893
|
function canPromptForRm(opts) {
|
|
@@ -4391,9 +4895,9 @@ function canPromptForRm(opts) {
|
|
|
4391
4895
|
return isInteractive() && Boolean(process.stdin.isTTY);
|
|
4392
4896
|
}
|
|
4393
4897
|
function removeOverviewRow(featurePlanDir, storyId) {
|
|
4394
|
-
const overview =
|
|
4395
|
-
if (!
|
|
4396
|
-
const src =
|
|
4898
|
+
const overview = path22.join(featurePlanDir, "00-overview.md");
|
|
4899
|
+
if (!fs28.existsSync(overview)) return;
|
|
4900
|
+
const src = fs28.readFileSync(overview, "utf8");
|
|
4397
4901
|
const lines = src.split("\n");
|
|
4398
4902
|
const filtered = lines.filter((line2) => {
|
|
4399
4903
|
if (!line2.startsWith("|")) return true;
|
|
@@ -4401,12 +4905,12 @@ function removeOverviewRow(featurePlanDir, storyId) {
|
|
|
4401
4905
|
if (/^\|\s*-+/.test(line2)) return true;
|
|
4402
4906
|
return !line2.includes(storyId);
|
|
4403
4907
|
});
|
|
4404
|
-
|
|
4908
|
+
fs28.writeFileSync(overview, filtered.join("\n"), "utf8");
|
|
4405
4909
|
}
|
|
4406
4910
|
function removeOverviewRowForPlanFile(featurePlanDir, planBasename) {
|
|
4407
|
-
const overview =
|
|
4408
|
-
if (!
|
|
4409
|
-
const src =
|
|
4911
|
+
const overview = path22.join(featurePlanDir, "00-overview.md");
|
|
4912
|
+
if (!fs28.existsSync(overview)) return;
|
|
4913
|
+
const src = fs28.readFileSync(overview, "utf8");
|
|
4410
4914
|
const token = `\`${planBasename}\``;
|
|
4411
4915
|
const lines = src.split("\n");
|
|
4412
4916
|
const filtered = lines.filter((line2) => {
|
|
@@ -4415,25 +4919,25 @@ function removeOverviewRowForPlanFile(featurePlanDir, planBasename) {
|
|
|
4415
4919
|
if (/^\|\s*-+/.test(line2)) return true;
|
|
4416
4920
|
return !line2.includes(token);
|
|
4417
4921
|
});
|
|
4418
|
-
|
|
4922
|
+
fs28.writeFileSync(overview, filtered.join("\n"), "utf8");
|
|
4419
4923
|
}
|
|
4420
4924
|
function trashOrDelete(absPaths, trashRoot, useTrash) {
|
|
4421
4925
|
if (!useTrash) {
|
|
4422
|
-
for (const p of absPaths)
|
|
4926
|
+
for (const p of absPaths) fs28.rmSync(p, { recursive: true, force: true });
|
|
4423
4927
|
return { removed: absPaths };
|
|
4424
4928
|
}
|
|
4425
4929
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
4426
|
-
const bucket =
|
|
4427
|
-
|
|
4930
|
+
const bucket = path22.join(trashRoot, ts);
|
|
4931
|
+
fs28.mkdirSync(bucket, { recursive: true });
|
|
4428
4932
|
for (const src of absPaths) {
|
|
4429
|
-
const rel =
|
|
4430
|
-
let dst =
|
|
4933
|
+
const rel = path22.basename(src);
|
|
4934
|
+
let dst = path22.join(bucket, rel);
|
|
4431
4935
|
let n = 0;
|
|
4432
|
-
while (
|
|
4936
|
+
while (fs28.existsSync(dst)) {
|
|
4433
4937
|
n += 1;
|
|
4434
|
-
dst =
|
|
4938
|
+
dst = path22.join(bucket, `${n}_${rel}`);
|
|
4435
4939
|
}
|
|
4436
|
-
|
|
4940
|
+
fs28.renameSync(src, dst);
|
|
4437
4941
|
}
|
|
4438
4942
|
return { removed: absPaths, trashed: bucket };
|
|
4439
4943
|
}
|
|
@@ -4444,21 +4948,21 @@ function renderPreview(kind, targets, opts) {
|
|
|
4444
4948
|
kv(t.kind, t.label, 6);
|
|
4445
4949
|
}
|
|
4446
4950
|
if (opts.trash) {
|
|
4447
|
-
info(`Items will move to ${
|
|
4951
|
+
info(`Items will move to ${path22.relative(process.cwd(), opts.trashRoot)}/<timestamp>/`);
|
|
4448
4952
|
} else if (!opts.dryRun) {
|
|
4449
4953
|
warning("This is a permanent delete. Pass --trash to move to .squad/.trash/ instead.");
|
|
4450
4954
|
}
|
|
4451
4955
|
}
|
|
4452
4956
|
function listAllPlanListEntries(paths, featureFilter) {
|
|
4453
|
-
if (!
|
|
4957
|
+
if (!fs28.existsSync(paths.plansDir)) return [];
|
|
4454
4958
|
const out = [];
|
|
4455
|
-
for (const ent of
|
|
4959
|
+
for (const ent of fs28.readdirSync(paths.plansDir, { withFileTypes: true })) {
|
|
4456
4960
|
if (!ent.isDirectory()) continue;
|
|
4457
4961
|
if (featureFilter && ent.name !== featureFilter) continue;
|
|
4458
|
-
const featureDir =
|
|
4459
|
-
for (const name of
|
|
4962
|
+
const featureDir = path22.join(paths.plansDir, ent.name);
|
|
4963
|
+
for (const name of fs28.readdirSync(featureDir)) {
|
|
4460
4964
|
if (!STORY_PLAN.test(name)) continue;
|
|
4461
|
-
out.push({ feature: ent.name, file: name, abs:
|
|
4965
|
+
out.push({ feature: ent.name, file: name, abs: path22.join(featureDir, name) });
|
|
4462
4966
|
}
|
|
4463
4967
|
}
|
|
4464
4968
|
return out.sort((a, b) => (a.feature + a.file).localeCompare(b.feature + b.file));
|
|
@@ -4509,34 +5013,34 @@ function resolveStoryFromArg(arg, stories, root) {
|
|
|
4509
5013
|
if (s.id === t) return s;
|
|
4510
5014
|
if (`${s.feature}/${s.id}` === t) return s;
|
|
4511
5015
|
}
|
|
4512
|
-
const abs =
|
|
4513
|
-
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");
|
|
4514
5018
|
return findStoryByIntake(stories, withIntake) ?? findStoryByIntake(stories, abs);
|
|
4515
5019
|
}
|
|
4516
5020
|
function isSequenceToken(s) {
|
|
4517
5021
|
return /^\d{1,4}$/.test(s) && s.length > 0;
|
|
4518
5022
|
}
|
|
4519
5023
|
function tryParsePlanFilePath(absPath, paths, featureFilter) {
|
|
4520
|
-
if (!
|
|
4521
|
-
const planRoot =
|
|
4522
|
-
const fileReal =
|
|
4523
|
-
const rel =
|
|
4524
|
-
if (rel.startsWith("..") ||
|
|
4525
|
-
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);
|
|
4526
5030
|
if (segs.length < 2) return void 0;
|
|
4527
5031
|
const feature = segs[0];
|
|
4528
|
-
const file = segs.slice(1).join(
|
|
5032
|
+
const file = segs.slice(1).join(path22.sep);
|
|
4529
5033
|
if (featureFilter && feature !== featureFilter) return void 0;
|
|
4530
|
-
return { feature, file, abs:
|
|
5034
|
+
return { feature, file, abs: path22.resolve(absPath) };
|
|
4531
5035
|
}
|
|
4532
5036
|
function resolvePlanFromArg(arg, paths, featureFilter) {
|
|
4533
5037
|
const t = arg.trim();
|
|
4534
5038
|
if (!t) return void 0;
|
|
4535
|
-
const asPath =
|
|
5039
|
+
const asPath = path22.isAbsolute(t) ? t : path22.resolve(process.cwd(), t);
|
|
4536
5040
|
const a = tryParsePlanFilePath(asPath, paths, featureFilter);
|
|
4537
5041
|
if (a) return a;
|
|
4538
|
-
if (!
|
|
4539
|
-
const under = tryParsePlanFilePath(
|
|
5042
|
+
if (!path22.isAbsolute(t)) {
|
|
5043
|
+
const under = tryParsePlanFilePath(path22.join(paths.plansDir, t), paths, featureFilter);
|
|
4540
5044
|
if (under) return under;
|
|
4541
5045
|
}
|
|
4542
5046
|
if (isSequenceToken(t)) {
|
|
@@ -4566,24 +5070,24 @@ function countOverviewDataRows(overviewContent) {
|
|
|
4566
5070
|
return n;
|
|
4567
5071
|
}
|
|
4568
5072
|
function featureStoriesDirEmpty(paths, feature) {
|
|
4569
|
-
const d =
|
|
4570
|
-
if (!
|
|
4571
|
-
return
|
|
5073
|
+
const d = path22.join(paths.storiesDir, feature);
|
|
5074
|
+
if (!fs28.existsSync(d)) return true;
|
|
5075
|
+
return fs28.readdirSync(d).length === 0;
|
|
4572
5076
|
}
|
|
4573
5077
|
|
|
4574
5078
|
// src/commands/rm/story.ts
|
|
4575
|
-
import
|
|
4576
|
-
import
|
|
5079
|
+
import fs30 from "fs";
|
|
5080
|
+
import path24 from "path";
|
|
4577
5081
|
import { confirm as confirm10 } from "@inquirer/prompts";
|
|
4578
5082
|
|
|
4579
5083
|
// src/commands/rm/feature.ts
|
|
4580
|
-
import
|
|
4581
|
-
import
|
|
5084
|
+
import fs29 from "fs";
|
|
5085
|
+
import path23 from "path";
|
|
4582
5086
|
import { confirm as confirm9 } from "@inquirer/prompts";
|
|
4583
5087
|
function countStoryDirs(storiesDir, feature) {
|
|
4584
|
-
const d =
|
|
4585
|
-
if (!
|
|
4586
|
-
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;
|
|
4587
5091
|
}
|
|
4588
5092
|
async function runRmFeature(featureSlug, opts = {}) {
|
|
4589
5093
|
const root = requireSquadRoot();
|
|
@@ -4617,17 +5121,17 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
4617
5121
|
feature = await pickFeatureInteractive(paths);
|
|
4618
5122
|
}
|
|
4619
5123
|
}
|
|
4620
|
-
const storyDir =
|
|
4621
|
-
const planDir =
|
|
5124
|
+
const storyDir = path23.join(paths.storiesDir, feature);
|
|
5125
|
+
const planDir = path23.join(paths.plansDir, feature);
|
|
4622
5126
|
const nStories = countStoryDirs(paths.storiesDir, feature);
|
|
4623
5127
|
const nPlans = listAllPlanListEntries(paths, feature).length;
|
|
4624
|
-
const hasOverview =
|
|
5128
|
+
const hasOverview = fs29.existsSync(path23.join(planDir, "00-overview.md")) ? 1 : 0;
|
|
4625
5129
|
const targets = [];
|
|
4626
|
-
if (
|
|
4627
|
-
targets.push({ label:
|
|
5130
|
+
if (fs29.existsSync(storyDir)) {
|
|
5131
|
+
targets.push({ label: path23.relative(root, storyDir), absPath: storyDir, kind: "dir" });
|
|
4628
5132
|
}
|
|
4629
|
-
if (
|
|
4630
|
-
targets.push({ label:
|
|
5133
|
+
if (fs29.existsSync(planDir)) {
|
|
5134
|
+
targets.push({ label: path23.relative(root, planDir), absPath: planDir, kind: "dir" });
|
|
4631
5135
|
}
|
|
4632
5136
|
divider("squad rm feature");
|
|
4633
5137
|
info(`Feature: ${feature}`);
|
|
@@ -4642,7 +5146,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
4642
5146
|
kv(t.kind, t.label, 6);
|
|
4643
5147
|
}
|
|
4644
5148
|
if (useTrash) {
|
|
4645
|
-
info(`Items will move to ${
|
|
5149
|
+
info(`Items will move to ${path23.relative(process.cwd(), paths.trashDir)}/<timestamp>/`);
|
|
4646
5150
|
} else if (!dryRun) {
|
|
4647
5151
|
warning("This is a permanent delete. Pass --trash to move to .squad/.trash/ instead.");
|
|
4648
5152
|
}
|
|
@@ -4675,7 +5179,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
4675
5179
|
useTrash
|
|
4676
5180
|
);
|
|
4677
5181
|
if (result.trashed) {
|
|
4678
|
-
success(`Feature removed. Items moved to ${
|
|
5182
|
+
success(`Feature removed. Items moved to ${path23.relative(root, result.trashed)}.`);
|
|
4679
5183
|
} else {
|
|
4680
5184
|
success("Feature removed (stories and plans directories).");
|
|
4681
5185
|
}
|
|
@@ -4716,11 +5220,11 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
4716
5220
|
}
|
|
4717
5221
|
}
|
|
4718
5222
|
const targets = [
|
|
4719
|
-
{ label:
|
|
5223
|
+
{ label: path24.relative(root, story.storyDir), absPath: story.storyDir, kind: "dir" }
|
|
4720
5224
|
];
|
|
4721
5225
|
if (story.planFile) {
|
|
4722
|
-
const planAbs =
|
|
4723
|
-
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" });
|
|
4724
5228
|
}
|
|
4725
5229
|
renderPreview("story", targets, {
|
|
4726
5230
|
dryRun,
|
|
@@ -4745,16 +5249,16 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
4745
5249
|
}
|
|
4746
5250
|
const absPaths = targets.map((t) => t.absPath);
|
|
4747
5251
|
const result = trashOrDelete(absPaths, paths.trashDir, useTrash);
|
|
4748
|
-
removeOverviewRow(
|
|
5252
|
+
removeOverviewRow(path24.join(paths.plansDir, story.feature), story.id);
|
|
4749
5253
|
if (result.trashed) {
|
|
4750
|
-
success(`Removed (moved to ${
|
|
5254
|
+
success(`Removed (moved to ${path24.relative(root, result.trashed)}).`);
|
|
4751
5255
|
} else {
|
|
4752
5256
|
success("Removed story folder, plan file (if any), and overview row.");
|
|
4753
5257
|
}
|
|
4754
|
-
const featurePlanDir =
|
|
4755
|
-
const overviewPath =
|
|
4756
|
-
if (
|
|
4757
|
-
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");
|
|
4758
5262
|
const onlyShell = countOverviewDataRows(body) === 0;
|
|
4759
5263
|
if (onlyShell && featureStoriesDirEmpty(paths, story.feature)) {
|
|
4760
5264
|
if (canPrompt) {
|
|
@@ -4774,7 +5278,7 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
4774
5278
|
}
|
|
4775
5279
|
|
|
4776
5280
|
// src/commands/rm/plan.ts
|
|
4777
|
-
import
|
|
5281
|
+
import path25 from "path";
|
|
4778
5282
|
import { confirm as confirm11 } from "@inquirer/prompts";
|
|
4779
5283
|
async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
4780
5284
|
const root = requireSquadRoot();
|
|
@@ -4810,7 +5314,7 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
4810
5314
|
}
|
|
4811
5315
|
const storyRows = listStories(paths, { feature: plan.feature });
|
|
4812
5316
|
const linked = storyRows.find((s) => s.planFile === plan.file);
|
|
4813
|
-
const targets = [{ label:
|
|
5317
|
+
const targets = [{ label: path25.relative(root, plan.abs), absPath: plan.abs, kind: "file" }];
|
|
4814
5318
|
renderPreview("plan", targets, {
|
|
4815
5319
|
dryRun,
|
|
4816
5320
|
trash: useTrash,
|
|
@@ -4833,14 +5337,14 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
4833
5337
|
}
|
|
4834
5338
|
}
|
|
4835
5339
|
trashOrDelete([plan.abs], paths.trashDir, useTrash);
|
|
4836
|
-
removeOverviewRowForPlanFile(
|
|
4837
|
-
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}/`;
|
|
4838
5342
|
success(`Plan removed. Run \`squad new-plan ${intakeHint}\` to regenerate.`);
|
|
4839
5343
|
}
|
|
4840
5344
|
|
|
4841
5345
|
// src/cli.ts
|
|
4842
5346
|
var program = new Command();
|
|
4843
|
-
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");
|
|
4844
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(
|
|
4845
5349
|
"--tracker-workspace <hostOrOrg>",
|
|
4846
5350
|
"Jira host or Azure org (non-interactive default; also written to config for jira/azure)"
|