squad-kit 0.3.0 → 0.4.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 +23 -0
- package/dist/cli.js +490 -69
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/agents/claude-code/squad-plan.md +1 -0
- package/templates/agents/copilot/squad-plan.prompt.md +2 -0
- package/templates/agents/cursor/squad-plan.md +1 -0
- package/templates/agents/gemini/squad-plan.toml +2 -0
- package/templates/prompts/generate-plan.md +3 -0
- package/templates/prompts/intake.md +11 -8
- package/templates/prompts/story-skeleton.md +2 -0
package/dist/cli.js
CHANGED
|
@@ -169,6 +169,19 @@ function renderError(err) {
|
|
|
169
169
|
`);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
// src/core/cli-exit.ts
|
|
173
|
+
var SquadExit = class extends Error {
|
|
174
|
+
constructor(exitCode, message) {
|
|
175
|
+
super(message ?? "");
|
|
176
|
+
this.exitCode = exitCode;
|
|
177
|
+
}
|
|
178
|
+
exitCode;
|
|
179
|
+
name = "SquadExit";
|
|
180
|
+
};
|
|
181
|
+
function isSquadExit(err) {
|
|
182
|
+
return err instanceof SquadExit;
|
|
183
|
+
}
|
|
184
|
+
|
|
172
185
|
// src/commands/init.ts
|
|
173
186
|
import fs14 from "fs";
|
|
174
187
|
import path6 from "path";
|
|
@@ -276,6 +289,18 @@ var DEFAULT_CONFIG = {
|
|
|
276
289
|
planner: void 0
|
|
277
290
|
};
|
|
278
291
|
var FORBIDDEN_KEYS = ["apikey", "api_key", "token", "secret", "credential", "credentials"];
|
|
292
|
+
var DEFAULT_PLANNER_MAX_OUTPUT_TOKENS = 16384;
|
|
293
|
+
var MIN_PLANNER_MAX_OUTPUT_TOKENS = 1024;
|
|
294
|
+
var MAX_PLANNER_MAX_OUTPUT_TOKENS = 128e3;
|
|
295
|
+
function clampPlannerMaxOut(raw) {
|
|
296
|
+
if (raw === void 0 || raw === null) return DEFAULT_PLANNER_MAX_OUTPUT_TOKENS;
|
|
297
|
+
const n = typeof raw === "number" ? raw : typeof raw === "string" ? Number.parseInt(raw, 10) : NaN;
|
|
298
|
+
if (!Number.isFinite(n) || n < 1) return DEFAULT_PLANNER_MAX_OUTPUT_TOKENS;
|
|
299
|
+
return Math.min(
|
|
300
|
+
MAX_PLANNER_MAX_OUTPUT_TOKENS,
|
|
301
|
+
Math.max(MIN_PLANNER_MAX_OUTPUT_TOKENS, Math.round(n))
|
|
302
|
+
);
|
|
303
|
+
}
|
|
279
304
|
function rejectSecretsInYaml(node, configFile, path26 = []) {
|
|
280
305
|
if (node === null || typeof node !== "object") return;
|
|
281
306
|
for (const [key, value] of Object.entries(node)) {
|
|
@@ -305,7 +330,8 @@ function mergePlanner(base, override) {
|
|
|
305
330
|
},
|
|
306
331
|
cache: {
|
|
307
332
|
enabled: override.cache?.enabled ?? base?.cache?.enabled ?? true
|
|
308
|
-
}
|
|
333
|
+
},
|
|
334
|
+
maxOutputTokens: clampPlannerMaxOut(override.maxOutputTokens ?? base?.maxOutputTokens)
|
|
309
335
|
};
|
|
310
336
|
if (merged.modelOverride) {
|
|
311
337
|
const pruned = Object.fromEntries(
|
|
@@ -584,6 +610,9 @@ function render(template, vars) {
|
|
|
584
610
|
return value === void 0 || value === null ? "" : String(value);
|
|
585
611
|
});
|
|
586
612
|
}
|
|
613
|
+
function escapeTemplateValue(s) {
|
|
614
|
+
return s.replace(/\{\{/g, "{ {");
|
|
615
|
+
}
|
|
587
616
|
|
|
588
617
|
// src/commands/config/show.ts
|
|
589
618
|
import fs7 from "fs";
|
|
@@ -856,7 +885,8 @@ function newPlannerBlock(provider) {
|
|
|
856
885
|
maxContextBytes: 5e4,
|
|
857
886
|
maxDurationSeconds: 180
|
|
858
887
|
},
|
|
859
|
-
cache: { enabled: true }
|
|
888
|
+
cache: { enabled: true },
|
|
889
|
+
maxOutputTokens: DEFAULT_PLANNER_MAX_OUTPUT_TOKENS
|
|
860
890
|
};
|
|
861
891
|
}
|
|
862
892
|
function mergePlannerKeyIntoSecrets(base, provider, key) {
|
|
@@ -1217,6 +1247,7 @@ var AzureDevOpsClient = class {
|
|
|
1217
1247
|
id: String(body.id ?? id),
|
|
1218
1248
|
title: fields["System.Title"] ?? "(no title)",
|
|
1219
1249
|
description: stripHtml(fields["System.Description"] ?? ""),
|
|
1250
|
+
acceptanceCriteria: stripHtml(fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? ""),
|
|
1220
1251
|
url: `${this.webBase}/${body.id ?? id}`,
|
|
1221
1252
|
labels: parseTags(fields["System.Tags"]),
|
|
1222
1253
|
type: fields["System.WorkItemType"],
|
|
@@ -1340,6 +1371,7 @@ var JiraClient = class {
|
|
|
1340
1371
|
id: body.key ?? id,
|
|
1341
1372
|
title: fields.summary ?? "(no title)",
|
|
1342
1373
|
description,
|
|
1374
|
+
acceptanceCriteria: "",
|
|
1343
1375
|
url: `${this.baseUrl}/browse/${body.key ?? id}`,
|
|
1344
1376
|
labels: Array.isArray(fields.labels) ? fields.labels : [],
|
|
1345
1377
|
type: fields.issuetype?.name,
|
|
@@ -1772,7 +1804,9 @@ async function runInit(opts) {
|
|
|
1772
1804
|
maxFileReads: 25,
|
|
1773
1805
|
maxContextBytes: 5e4,
|
|
1774
1806
|
maxDurationSeconds: 180
|
|
1775
|
-
}
|
|
1807
|
+
},
|
|
1808
|
+
cache: { enabled: true },
|
|
1809
|
+
maxOutputTokens: DEFAULT_PLANNER_MAX_OUTPUT_TOKENS
|
|
1776
1810
|
};
|
|
1777
1811
|
}
|
|
1778
1812
|
} else {
|
|
@@ -1798,7 +1832,9 @@ async function runInit(opts) {
|
|
|
1798
1832
|
maxFileReads: 25,
|
|
1799
1833
|
maxContextBytes: 5e4,
|
|
1800
1834
|
maxDurationSeconds: 180
|
|
1801
|
-
}
|
|
1835
|
+
},
|
|
1836
|
+
cache: { enabled: true },
|
|
1837
|
+
maxOutputTokens: DEFAULT_PLANNER_MAX_OUTPUT_TOKENS
|
|
1802
1838
|
};
|
|
1803
1839
|
}
|
|
1804
1840
|
}
|
|
@@ -1969,11 +2005,14 @@ function listStories(paths, opts = {}) {
|
|
|
1969
2005
|
function findPlanFor(plansDir, feature, storyId) {
|
|
1970
2006
|
const featureDir = path7.join(plansDir, feature);
|
|
1971
2007
|
if (!fs15.existsSync(featureDir)) return void 0;
|
|
2008
|
+
const candidates = [];
|
|
1972
2009
|
for (const entry of fs15.readdirSync(featureDir)) {
|
|
1973
2010
|
if (!STORY_FILE.test(entry)) continue;
|
|
1974
|
-
if (entry.includes(storyId))
|
|
2011
|
+
if (entry.includes(storyId)) candidates.push(entry);
|
|
1975
2012
|
}
|
|
1976
|
-
return void 0;
|
|
2013
|
+
if (candidates.length === 0) return void 0;
|
|
2014
|
+
const complete = candidates.find((e) => !e.endsWith(".partial.md"));
|
|
2015
|
+
return complete ?? candidates[0];
|
|
1977
2016
|
}
|
|
1978
2017
|
function findStoryByIntake(records, intakePath) {
|
|
1979
2018
|
const target = normalizeIntakePath(intakePath.trim());
|
|
@@ -1989,21 +2028,53 @@ function normalizeIntakePath(p) {
|
|
|
1989
2028
|
}
|
|
1990
2029
|
function readTitleHint(intakePath) {
|
|
1991
2030
|
try {
|
|
1992
|
-
const
|
|
1993
|
-
const
|
|
1994
|
-
|
|
2031
|
+
const raw = fs15.readFileSync(intakePath, "utf8");
|
|
2032
|
+
const head = raw.split("\n").slice(0, 30).join("\n");
|
|
2033
|
+
const hintLine = head.match(/>\s*\*\*Title hint \(from CLI\):\*\*\s*(.+?)\s*$/m);
|
|
2034
|
+
const fromHint = hintLine?.[1]?.trim();
|
|
2035
|
+
if (fromHint) return fromHint;
|
|
2036
|
+
const titleFence = raw.match(/(?:^|\n)## Title\b[\s\S]*?\n```(?:[\w-]*)?\s*\n([\s\S]*?)\n```/);
|
|
2037
|
+
const fromFence = titleFence?.[1]?.trim();
|
|
2038
|
+
if (fromFence) return fromFence;
|
|
2039
|
+
return void 0;
|
|
1995
2040
|
} catch {
|
|
1996
2041
|
return void 0;
|
|
1997
2042
|
}
|
|
1998
2043
|
}
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2044
|
+
var PLAN_GENERATED_BY = /^<!--\s*squad-kit:\s*generated by\s+([^\s,]+)/;
|
|
2045
|
+
var PLAN_ANY_SQUAD = /^<!--\s*squad-kit:\s*(.+?)\s*-->$/;
|
|
2046
|
+
var MAX_FREEFORM_META_LEN = 96;
|
|
2047
|
+
function parsePlanMetadataLine(line2) {
|
|
2048
|
+
const strict = line2.match(PLAN_GENERATED_BY);
|
|
2049
|
+
if (strict) {
|
|
2050
|
+
const generatedBy = strict[1];
|
|
2005
2051
|
const [provider, model] = generatedBy.split("/");
|
|
2006
2052
|
return { provider, model, generatedBy };
|
|
2053
|
+
}
|
|
2054
|
+
const any = line2.match(PLAN_ANY_SQUAD);
|
|
2055
|
+
if (!any) return null;
|
|
2056
|
+
const inner = any[1].trim();
|
|
2057
|
+
const flex = inner.match(/^generated\s+by\s+([^\s,]+)/i);
|
|
2058
|
+
if (flex) {
|
|
2059
|
+
const generatedBy = flex[1];
|
|
2060
|
+
const [provider, model] = generatedBy.split("/");
|
|
2061
|
+
return { provider, model, generatedBy };
|
|
2062
|
+
}
|
|
2063
|
+
const short = inner.length > MAX_FREEFORM_META_LEN ? `${inner.slice(0, MAX_FREEFORM_META_LEN - 1)}\u2026` : inner;
|
|
2064
|
+
return { generatedBy: short };
|
|
2065
|
+
}
|
|
2066
|
+
function readPlanMetadata(planFileAbsPath) {
|
|
2067
|
+
try {
|
|
2068
|
+
const raw = fs15.readFileSync(planFileAbsPath, "utf8").replace(/^\uFEFF/, "");
|
|
2069
|
+
const lines = raw.split(/\n/);
|
|
2070
|
+
const scan = Math.min(lines.length, 40);
|
|
2071
|
+
for (let i = 0; i < scan; i++) {
|
|
2072
|
+
const line2 = (lines[i] ?? "").replace(/\r$/, "").trim();
|
|
2073
|
+
if (!line2) continue;
|
|
2074
|
+
const parsed = parsePlanMetadataLine(line2);
|
|
2075
|
+
if (parsed) return parsed;
|
|
2076
|
+
}
|
|
2077
|
+
return {};
|
|
2007
2078
|
} catch {
|
|
2008
2079
|
return {};
|
|
2009
2080
|
}
|
|
@@ -2143,10 +2214,12 @@ async function runNewStory(featureSlug, opts) {
|
|
|
2143
2214
|
fs16.mkdirSync(attachmentsDir, { recursive: true });
|
|
2144
2215
|
let sourceBlock = "";
|
|
2145
2216
|
let fetchedSummary;
|
|
2217
|
+
let fetchedIssue;
|
|
2146
2218
|
const shouldFetch = !noTracker && opts.fetch !== false && Boolean(id) && config2.tracker.type !== "none";
|
|
2147
2219
|
if (shouldFetch) {
|
|
2148
2220
|
const outcome = await tryFetchIssue(config2, id, opts, paths, attachmentsDir);
|
|
2149
2221
|
if (outcome.outcome === "ok") {
|
|
2222
|
+
fetchedIssue = outcome.issue;
|
|
2150
2223
|
sourceBlock = buildSourcePreamble(outcome.issue, outcome.downloads, config2.tracker.type);
|
|
2151
2224
|
if (!title) title = outcome.issue.title;
|
|
2152
2225
|
const written = outcome.downloads.filter((d) => d.outcome === "written").length;
|
|
@@ -2158,18 +2231,19 @@ async function runNewStory(featureSlug, opts) {
|
|
|
2158
2231
|
}
|
|
2159
2232
|
}
|
|
2160
2233
|
const template = readBundledPrompt("intake.md");
|
|
2161
|
-
const rendered = render(
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2234
|
+
const rendered = render(
|
|
2235
|
+
template,
|
|
2236
|
+
buildIntakeTemplateVars({
|
|
2237
|
+
config: config2,
|
|
2238
|
+
featureSlug: slug,
|
|
2239
|
+
storyFolderName,
|
|
2240
|
+
trackerId: id,
|
|
2241
|
+
title,
|
|
2242
|
+
fetchedIssue
|
|
2243
|
+
})
|
|
2244
|
+
);
|
|
2168
2245
|
const skippedPreamble = noTracker && config2.tracker.type !== "none" && !sourceBlock ? buildSkippedTrackerPreamble(config2.tracker.type) : "";
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
` : "";
|
|
2172
|
-
writeFileSafe(intakeFile, skippedPreamble + sourceBlock + titleHint + rendered, false);
|
|
2246
|
+
writeFileSafe(intakeFile, skippedPreamble + sourceBlock + rendered, false);
|
|
2173
2247
|
ensureFeatureOverview(path8.join(paths.plansDir, slug), slug);
|
|
2174
2248
|
const relativePath = path8.relative(root, intakeFile);
|
|
2175
2249
|
const relAttachDir = path8.relative(root, attachmentsDir);
|
|
@@ -2242,6 +2316,25 @@ function explainTrackerErrorHint(err) {
|
|
|
2242
2316
|
function truncate(s, n) {
|
|
2243
2317
|
return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
|
|
2244
2318
|
}
|
|
2319
|
+
function buildIntakeTemplateVars(args) {
|
|
2320
|
+
const e = escapeTemplateValue;
|
|
2321
|
+
const { config: config2, featureSlug, storyFolderName, trackerId, title, fetchedIssue } = args;
|
|
2322
|
+
return {
|
|
2323
|
+
featureSlug,
|
|
2324
|
+
storyId: storyFolderName,
|
|
2325
|
+
trackerType: config2.tracker.type,
|
|
2326
|
+
trackerWorkItemId: e((fetchedIssue?.id ?? trackerId ?? "").trim()),
|
|
2327
|
+
trackerWorkItemType: e((fetchedIssue?.type ?? "").trim()),
|
|
2328
|
+
trackerStatus: e((fetchedIssue?.status ?? "").trim()),
|
|
2329
|
+
trackerAssignee: e((fetchedIssue?.assignee ?? "").trim()),
|
|
2330
|
+
trackerLabels: e((fetchedIssue?.labels ?? []).join(", ").trim()),
|
|
2331
|
+
trackerTitle: e((title ?? "").trim()),
|
|
2332
|
+
trackerDescription: e((fetchedIssue?.description ?? "").trim()),
|
|
2333
|
+
trackerAcceptanceCriteria: e((fetchedIssue?.acceptanceCriteria ?? "").trim()),
|
|
2334
|
+
projectRoots: (config2.project.projectRoots ?? ["."]).join(", "),
|
|
2335
|
+
primaryLanguage: config2.project.primaryLanguage ?? ""
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2245
2338
|
function buildSourcePreamble(issue, downloads, trackerType) {
|
|
2246
2339
|
const header = `> **Fetched from ${trackerType}:** [${issue.id}](${issue.url})
|
|
2247
2340
|
> *Fetched ${issue.fetchedAt}. Edit the sections below as needed; the planner reads this file verbatim.*
|
|
@@ -2965,6 +3058,10 @@ function prefixOf(provider, req) {
|
|
|
2965
3058
|
return s.length > 0 ? s.slice(0, -1) : s;
|
|
2966
3059
|
}
|
|
2967
3060
|
|
|
3061
|
+
// src/planner/session-limits.ts
|
|
3062
|
+
var DEFAULT_PLANNER_MAX_ITERATIONS = 40;
|
|
3063
|
+
var PLANNER_MARKDOWN_CONTINUATION_USER = "Your previous reply was cut off because it hit the per-response output token limit for this request. Continue the Markdown plan from the exact cut-off only: write the remainder, do not repeat headings or paragraphs already sent. When the plan is complete, use end_turn without unnecessary tool calls.";
|
|
3064
|
+
|
|
2968
3065
|
// src/planner/loop.ts
|
|
2969
3066
|
var MAX_RATE_LIMIT_RETRY_SEC = 90;
|
|
2970
3067
|
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -2986,18 +3083,83 @@ function buildPlannerRunStats(budget, turns, runStartedAt) {
|
|
|
2986
3083
|
durationMs: Date.now() - runStartedAt
|
|
2987
3084
|
};
|
|
2988
3085
|
}
|
|
3086
|
+
function readBudgetishError(result) {
|
|
3087
|
+
return result.isError && (/budget/i.test(result.content) || /max file reads/i.test(result.content));
|
|
3088
|
+
}
|
|
3089
|
+
function extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut) {
|
|
3090
|
+
input5.budget.extendSession();
|
|
3091
|
+
maxIter.current += baseMaxIter;
|
|
3092
|
+
maxOut.current += baseMaxOut;
|
|
3093
|
+
}
|
|
2989
3094
|
async function runPlanner(input5) {
|
|
2990
3095
|
const runStartedAt = Date.now();
|
|
2991
|
-
const
|
|
3096
|
+
const baseMaxIter = input5.maxIterations ?? DEFAULT_PLANNER_MAX_ITERATIONS;
|
|
3097
|
+
const maxIter = { current: baseMaxIter };
|
|
3098
|
+
const baseMaxOut = input5.maxOutputTokens ?? DEFAULT_PLANNER_MAX_OUTPUT_TOKENS;
|
|
3099
|
+
const maxOut = { current: baseMaxOut };
|
|
2992
3100
|
const turns = [{ role: "user", text: input5.userPrompt }];
|
|
2993
3101
|
let accumulatedText = "";
|
|
2994
3102
|
let iterations = 0;
|
|
2995
3103
|
let budgetExhausted = false;
|
|
2996
3104
|
let finishedNormally = false;
|
|
2997
3105
|
let prevRequestPrefix;
|
|
2998
|
-
|
|
3106
|
+
const decide = input5.decideOnLimit;
|
|
3107
|
+
const limitCtx = (kind) => ({
|
|
3108
|
+
kind,
|
|
3109
|
+
budgetSnapshot: input5.budget.snapshot(),
|
|
3110
|
+
iterations,
|
|
3111
|
+
maxIterations: maxIter.current,
|
|
3112
|
+
maxOutputTokens: maxOut.current
|
|
3113
|
+
});
|
|
3114
|
+
for (; ; ) {
|
|
3115
|
+
if (iterations >= maxIter.current) {
|
|
3116
|
+
if (decide) {
|
|
3117
|
+
const d = await decide(limitCtx("max_iterations"));
|
|
3118
|
+
if (d === "cancel") {
|
|
3119
|
+
return {
|
|
3120
|
+
planText: accumulatedText,
|
|
3121
|
+
budgetExhausted,
|
|
3122
|
+
timedOut: false,
|
|
3123
|
+
finishedNormally: false,
|
|
3124
|
+
iterations,
|
|
3125
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
|
|
3126
|
+
incompleteKind: "max_iterations",
|
|
3127
|
+
userCancelled: true
|
|
3128
|
+
};
|
|
3129
|
+
}
|
|
3130
|
+
extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut);
|
|
3131
|
+
continue;
|
|
3132
|
+
}
|
|
3133
|
+
return {
|
|
3134
|
+
planText: accumulatedText,
|
|
3135
|
+
budgetExhausted,
|
|
3136
|
+
timedOut: false,
|
|
3137
|
+
finishedNormally: false,
|
|
3138
|
+
iterations,
|
|
3139
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
|
|
3140
|
+
incompleteKind: "max_iterations"
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
2999
3143
|
iterations += 1;
|
|
3000
3144
|
if (input5.budget.timedOut()) {
|
|
3145
|
+
if (decide) {
|
|
3146
|
+
const d = await decide(limitCtx("wall_clock"));
|
|
3147
|
+
if (d === "cancel") {
|
|
3148
|
+
return {
|
|
3149
|
+
planText: accumulatedText,
|
|
3150
|
+
budgetExhausted,
|
|
3151
|
+
timedOut: true,
|
|
3152
|
+
finishedNormally: false,
|
|
3153
|
+
iterations,
|
|
3154
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
|
|
3155
|
+
incompleteKind: "wall_clock",
|
|
3156
|
+
userCancelled: true
|
|
3157
|
+
};
|
|
3158
|
+
}
|
|
3159
|
+
extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut);
|
|
3160
|
+
iterations -= 1;
|
|
3161
|
+
continue;
|
|
3162
|
+
}
|
|
3001
3163
|
return {
|
|
3002
3164
|
planText: accumulatedText,
|
|
3003
3165
|
budgetExhausted,
|
|
@@ -3008,6 +3170,24 @@ async function runPlanner(input5) {
|
|
|
3008
3170
|
};
|
|
3009
3171
|
}
|
|
3010
3172
|
if (input5.budget.overCost()) {
|
|
3173
|
+
if (decide) {
|
|
3174
|
+
const d = await decide(limitCtx("cost_cap"));
|
|
3175
|
+
if (d === "cancel") {
|
|
3176
|
+
return {
|
|
3177
|
+
planText: accumulatedText,
|
|
3178
|
+
budgetExhausted,
|
|
3179
|
+
timedOut: false,
|
|
3180
|
+
finishedNormally: false,
|
|
3181
|
+
iterations,
|
|
3182
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
|
|
3183
|
+
incompleteKind: "cost_cap",
|
|
3184
|
+
userCancelled: true
|
|
3185
|
+
};
|
|
3186
|
+
}
|
|
3187
|
+
extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut);
|
|
3188
|
+
iterations -= 1;
|
|
3189
|
+
continue;
|
|
3190
|
+
}
|
|
3011
3191
|
return {
|
|
3012
3192
|
planText: accumulatedText,
|
|
3013
3193
|
budgetExhausted: true,
|
|
@@ -3023,7 +3203,7 @@ async function runPlanner(input5) {
|
|
|
3023
3203
|
tools: [READ_FILE_TOOL],
|
|
3024
3204
|
turns,
|
|
3025
3205
|
apiKey: input5.apiKey,
|
|
3026
|
-
maxOutputTokens:
|
|
3206
|
+
maxOutputTokens: maxOut.current,
|
|
3027
3207
|
cacheEnabled: input5.cacheEnabled ?? true
|
|
3028
3208
|
};
|
|
3029
3209
|
if (process.env.NODE_ENV !== "production" && process.env.NODE_ENV !== "test" && prevRequestPrefix !== void 0) {
|
|
@@ -3067,13 +3247,34 @@ async function runPlanner(input5) {
|
|
|
3067
3247
|
input5.onAssistantText?.(response.text);
|
|
3068
3248
|
}
|
|
3069
3249
|
if (response.stopReason === "max_tokens" && !response.toolCalls?.length) {
|
|
3250
|
+
if (decide) {
|
|
3251
|
+
const d = await decide(limitCtx("max_output_tokens"));
|
|
3252
|
+
if (d === "cancel") {
|
|
3253
|
+
return {
|
|
3254
|
+
planText: accumulatedText,
|
|
3255
|
+
budgetExhausted,
|
|
3256
|
+
timedOut: false,
|
|
3257
|
+
finishedNormally: false,
|
|
3258
|
+
iterations,
|
|
3259
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
|
|
3260
|
+
incompleteKind: "max_output_tokens",
|
|
3261
|
+
userCancelled: true
|
|
3262
|
+
};
|
|
3263
|
+
}
|
|
3264
|
+
extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut);
|
|
3265
|
+
turns.push({ role: "assistant", text: response.text ?? "" });
|
|
3266
|
+
turns.push({ role: "user", text: PLANNER_MARKDOWN_CONTINUATION_USER });
|
|
3267
|
+
iterations -= 1;
|
|
3268
|
+
continue;
|
|
3269
|
+
}
|
|
3070
3270
|
return {
|
|
3071
3271
|
planText: accumulatedText,
|
|
3072
3272
|
budgetExhausted,
|
|
3073
3273
|
timedOut: false,
|
|
3074
3274
|
finishedNormally: false,
|
|
3075
3275
|
iterations,
|
|
3076
|
-
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
3276
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
|
|
3277
|
+
incompleteKind: "max_output_tokens"
|
|
3077
3278
|
};
|
|
3078
3279
|
}
|
|
3079
3280
|
if (response.stopReason === "end_turn" || !response.toolCalls?.length) {
|
|
@@ -3093,17 +3294,47 @@ async function runPlanner(input5) {
|
|
|
3093
3294
|
toolCalls: response.toolCalls
|
|
3094
3295
|
});
|
|
3095
3296
|
const toolResults = [];
|
|
3096
|
-
|
|
3297
|
+
const toolCalls = response.toolCalls;
|
|
3298
|
+
for (let i = 0; i < toolCalls.length; i += 1) {
|
|
3299
|
+
const tc = toolCalls[i];
|
|
3097
3300
|
if (tc.name !== READ_FILE_TOOL.name) {
|
|
3098
3301
|
toolResults.push({ toolCallId: tc.id, content: `unknown tool "${tc.name}"`, isError: true });
|
|
3099
3302
|
continue;
|
|
3100
3303
|
}
|
|
3101
3304
|
const before = input5.budget.snapshot().bytes;
|
|
3102
|
-
|
|
3305
|
+
let result = readFileTool(input5.root, input5.budget, tc.input);
|
|
3103
3306
|
const after = input5.budget.snapshot().bytes;
|
|
3104
3307
|
input5.onToolCall?.(tc, after - before, after);
|
|
3105
|
-
if (
|
|
3106
|
-
|
|
3308
|
+
if (readBudgetishError(result)) {
|
|
3309
|
+
if (decide) {
|
|
3310
|
+
const d = await decide(limitCtx("file_or_context_reads"));
|
|
3311
|
+
if (d === "cancel") {
|
|
3312
|
+
toolResults.push({ toolCallId: tc.id, content: result.content, isError: true });
|
|
3313
|
+
for (let j = i + 1; j < toolCalls.length; j += 1) {
|
|
3314
|
+
const t2 = toolCalls[j];
|
|
3315
|
+
toolResults.push({
|
|
3316
|
+
toolCallId: t2.id,
|
|
3317
|
+
content: t2.name === READ_FILE_TOOL.name ? "read_file: not executed (planning session stopped after file/context budget)." : `unknown tool "${t2.name}"`,
|
|
3318
|
+
isError: true
|
|
3319
|
+
});
|
|
3320
|
+
}
|
|
3321
|
+
turns.push({ role: "user", toolResults });
|
|
3322
|
+
return {
|
|
3323
|
+
planText: accumulatedText,
|
|
3324
|
+
budgetExhausted: false,
|
|
3325
|
+
timedOut: false,
|
|
3326
|
+
finishedNormally: false,
|
|
3327
|
+
iterations,
|
|
3328
|
+
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt),
|
|
3329
|
+
incompleteKind: "budget_reads",
|
|
3330
|
+
userCancelled: true
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3333
|
+
extendAllSessionLimits(input5, maxIter, maxOut, baseMaxIter, baseMaxOut);
|
|
3334
|
+
result = readFileTool(input5.root, input5.budget, tc.input);
|
|
3335
|
+
} else {
|
|
3336
|
+
budgetExhausted = true;
|
|
3337
|
+
}
|
|
3107
3338
|
}
|
|
3108
3339
|
toolResults.push({ toolCallId: tc.id, content: result.content, isError: result.isError });
|
|
3109
3340
|
}
|
|
@@ -3115,14 +3346,6 @@ async function runPlanner(input5) {
|
|
|
3115
3346
|
});
|
|
3116
3347
|
}
|
|
3117
3348
|
}
|
|
3118
|
-
return {
|
|
3119
|
-
planText: accumulatedText,
|
|
3120
|
-
budgetExhausted,
|
|
3121
|
-
timedOut: false,
|
|
3122
|
-
finishedNormally: false,
|
|
3123
|
-
iterations,
|
|
3124
|
-
stats: buildPlannerRunStats(input5.budget, iterations, runStartedAt)
|
|
3125
|
-
};
|
|
3126
3349
|
}
|
|
3127
3350
|
function firstByteDiff(a, b) {
|
|
3128
3351
|
const n = Math.min(a.length, b.length);
|
|
@@ -3151,6 +3374,84 @@ function composePlannerError(providerName, response, retriedRateLimit, retrySkip
|
|
|
3151
3374
|
return new Error(`${base} Run \`squad doctor\` to diagnose, or retry \u2014 most 5xx errors are transient.`);
|
|
3152
3375
|
}
|
|
3153
3376
|
|
|
3377
|
+
// src/planner/planner-limit-messages.ts
|
|
3378
|
+
function printPlannerApiCostNotice() {
|
|
3379
|
+
blank();
|
|
3380
|
+
step("Billing");
|
|
3381
|
+
info(
|
|
3382
|
+
"`squad new-plan --api` sends your intake and repo context to the configured provider. Each model round is billed like any other API usage (input tokens, output tokens, and cache-related tokens when applicable). Choosing \u201CContinue\u201D after a limit runs more rounds and usually increases cost."
|
|
3383
|
+
);
|
|
3384
|
+
blank();
|
|
3385
|
+
}
|
|
3386
|
+
function kindTitle(kind) {
|
|
3387
|
+
switch (kind) {
|
|
3388
|
+
case "max_output_tokens":
|
|
3389
|
+
return "Output length limit (single response)";
|
|
3390
|
+
case "max_iterations":
|
|
3391
|
+
return "Model round limit";
|
|
3392
|
+
case "wall_clock":
|
|
3393
|
+
return "Wall-clock time limit";
|
|
3394
|
+
case "cost_cap":
|
|
3395
|
+
return "Estimated cost cap";
|
|
3396
|
+
case "file_or_context_reads":
|
|
3397
|
+
return "File read or context size limit";
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
function kindDetail(ctx) {
|
|
3401
|
+
const snap = ctx.budgetSnapshot;
|
|
3402
|
+
const cost = snap.usage.costUsd !== void 0 ? `Estimated spend so far: about $${snap.usage.costUsd.toFixed(2)} (provider-reported when available).` : "Estimated spend so far: not reported by the provider for this run.";
|
|
3403
|
+
switch (ctx.kind) {
|
|
3404
|
+
case "max_output_tokens":
|
|
3405
|
+
return [
|
|
3406
|
+
`The model hit the per-request output cap (${ctx.maxOutputTokens} completion tokens). Long plans can stop mid-markdown even though the session is otherwise healthy.`,
|
|
3407
|
+
"You can continue: squad-kit will ask the model to append the rest of the plan in a follow-up request (extra tokens apply).",
|
|
3408
|
+
cost
|
|
3409
|
+
];
|
|
3410
|
+
case "max_iterations":
|
|
3411
|
+
return [
|
|
3412
|
+
`The planner reached its round cap (${ctx.maxIterations} model turns) without a clean stop.`,
|
|
3413
|
+
"Continuing raises the round cap and file/time budgets for this run by another full slice from your config.",
|
|
3414
|
+
cost
|
|
3415
|
+
];
|
|
3416
|
+
case "wall_clock":
|
|
3417
|
+
return [
|
|
3418
|
+
"The configured wall-clock budget for this planning session elapsed before the model finished.",
|
|
3419
|
+
"Continuing adds another slice of time (and read/output/round limits) from your planner budget.",
|
|
3420
|
+
cost
|
|
3421
|
+
];
|
|
3422
|
+
case "cost_cap":
|
|
3423
|
+
return [
|
|
3424
|
+
"Spending crossed the optional `planner.budget.maxCostUsd` ceiling for this session.",
|
|
3425
|
+
"Continuing raises that ceiling for this run by another slice from config (if a cap is set).",
|
|
3426
|
+
cost
|
|
3427
|
+
];
|
|
3428
|
+
case "file_or_context_reads":
|
|
3429
|
+
return [
|
|
3430
|
+
"A `read_file` call could not run because the file-read count or total read-bytes budget was exhausted.",
|
|
3431
|
+
"Continuing extends read, context, time, round, and output limits for this session so the model can read again or finish without reads.",
|
|
3432
|
+
cost
|
|
3433
|
+
];
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
function printPlannerLimitExplanation(ctx) {
|
|
3437
|
+
warning(kindTitle(ctx.kind));
|
|
3438
|
+
for (const line2 of kindDetail(ctx)) {
|
|
3439
|
+
info(line2);
|
|
3440
|
+
}
|
|
3441
|
+
kv("session so far", `${snapSummary(ctx)}`, 18);
|
|
3442
|
+
}
|
|
3443
|
+
function snapSummary(ctx) {
|
|
3444
|
+
const s = ctx.budgetSnapshot;
|
|
3445
|
+
return `${s.reads} reads \xB7 ${(s.bytes / 1024).toFixed(1)} KB read context \xB7 ${s.usage.inputTokens} in / ${s.usage.outputTokens} out tokens`;
|
|
3446
|
+
}
|
|
3447
|
+
function printPlannerLimitNextSteps() {
|
|
3448
|
+
blank();
|
|
3449
|
+
step("If you stop here");
|
|
3450
|
+
info("An incomplete plan is saved as `*.partial.md` with front matter `squad-kit-plan-status: partial`.");
|
|
3451
|
+
info("Fix limits in `.squad/config.yaml` (`planner.budget`, `planner.maxOutputTokens`) or run again when ready.");
|
|
3452
|
+
blank();
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3154
3455
|
// src/planner/providers/index.ts
|
|
3155
3456
|
function providerFor(name) {
|
|
3156
3457
|
switch (name) {
|
|
@@ -3167,18 +3468,35 @@ function providerFor(name) {
|
|
|
3167
3468
|
var Budget = class {
|
|
3168
3469
|
constructor(cfg) {
|
|
3169
3470
|
this.cfg = cfg;
|
|
3471
|
+
this.maxFileReadsAllowed = cfg.maxFileReads;
|
|
3472
|
+
this.maxContextBytesAllowed = cfg.maxContextBytes;
|
|
3473
|
+
this.maxWallSeconds = cfg.maxDurationSeconds;
|
|
3474
|
+
this.maxCostUsdCeiling = cfg.maxCostUsd;
|
|
3170
3475
|
}
|
|
3171
3476
|
cfg;
|
|
3172
3477
|
reads = 0;
|
|
3173
3478
|
bytes = 0;
|
|
3174
3479
|
startedAt = Date.now();
|
|
3175
3480
|
totalUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
|
|
3481
|
+
maxFileReadsAllowed;
|
|
3482
|
+
maxContextBytesAllowed;
|
|
3483
|
+
maxWallSeconds;
|
|
3484
|
+
maxCostUsdCeiling;
|
|
3485
|
+
/** Add another slice of the original config limits (used when the user chooses to continue a planner session). */
|
|
3486
|
+
extendSession() {
|
|
3487
|
+
this.maxFileReadsAllowed += this.cfg.maxFileReads;
|
|
3488
|
+
this.maxContextBytesAllowed += this.cfg.maxContextBytes;
|
|
3489
|
+
this.maxWallSeconds += this.cfg.maxDurationSeconds;
|
|
3490
|
+
if (this.cfg.maxCostUsd !== void 0) {
|
|
3491
|
+
this.maxCostUsdCeiling = (this.maxCostUsdCeiling ?? 0) + this.cfg.maxCostUsd;
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3176
3494
|
canRead(nextFileBytes) {
|
|
3177
|
-
if (this.reads >= this.
|
|
3178
|
-
return { ok: false, reason: `max file reads (${this.
|
|
3495
|
+
if (this.reads >= this.maxFileReadsAllowed) {
|
|
3496
|
+
return { ok: false, reason: `max file reads (${this.maxFileReadsAllowed}) reached` };
|
|
3179
3497
|
}
|
|
3180
|
-
if (this.bytes + nextFileBytes > this.
|
|
3181
|
-
return { ok: false, reason: `context budget (${this.
|
|
3498
|
+
if (this.bytes + nextFileBytes > this.maxContextBytesAllowed) {
|
|
3499
|
+
return { ok: false, reason: `context budget (${this.maxContextBytesAllowed} bytes) would be exceeded` };
|
|
3182
3500
|
}
|
|
3183
3501
|
return { ok: true };
|
|
3184
3502
|
}
|
|
@@ -3194,11 +3512,11 @@ var Budget = class {
|
|
|
3194
3512
|
this.totalUsage.cacheReadTokens = (this.totalUsage.cacheReadTokens ?? 0) + (u.cacheReadTokens ?? 0);
|
|
3195
3513
|
}
|
|
3196
3514
|
timedOut() {
|
|
3197
|
-
return (Date.now() - this.startedAt) / 1e3 > this.
|
|
3515
|
+
return (Date.now() - this.startedAt) / 1e3 > this.maxWallSeconds;
|
|
3198
3516
|
}
|
|
3199
3517
|
overCost() {
|
|
3200
|
-
if (this.
|
|
3201
|
-
return (this.totalUsage.costUsd ?? 0) > this.
|
|
3518
|
+
if (this.maxCostUsdCeiling === void 0) return false;
|
|
3519
|
+
return (this.totalUsage.costUsd ?? 0) > this.maxCostUsdCeiling;
|
|
3202
3520
|
}
|
|
3203
3521
|
snapshot() {
|
|
3204
3522
|
return {
|
|
@@ -3337,10 +3655,19 @@ function formatSequence(n) {
|
|
|
3337
3655
|
}
|
|
3338
3656
|
|
|
3339
3657
|
// src/planner/writer.ts
|
|
3658
|
+
function planFileSlugParts(config2, story) {
|
|
3659
|
+
const baseSlug = slugify(story.id);
|
|
3660
|
+
const trackerId = config2.naming.includeTrackerId && config2.tracker.type !== "none" ? trackerIdForFilename(config2.tracker.type, story.id) : "";
|
|
3661
|
+
const trackerPart = trackerId && trackerId.toLowerCase() !== baseSlug ? `-${trackerId}` : "";
|
|
3662
|
+
return { baseSlug, trackerPart };
|
|
3663
|
+
}
|
|
3340
3664
|
function writePlanFile(input5) {
|
|
3341
3665
|
const { paths, config: config2, story } = input5;
|
|
3342
3666
|
const featurePlanDir = path13.join(paths.plansDir, story.feature);
|
|
3343
3667
|
fs20.mkdirSync(featurePlanDir, { recursive: true });
|
|
3668
|
+
if (input5.partial) {
|
|
3669
|
+
return writePartialPlanFile(input5, featurePlanDir, paths, config2, story);
|
|
3670
|
+
}
|
|
3344
3671
|
const existing = findExistingPlan(featurePlanDir, story.id);
|
|
3345
3672
|
let targetPath;
|
|
3346
3673
|
let nn;
|
|
@@ -3351,10 +3678,7 @@ function writePlanFile(input5) {
|
|
|
3351
3678
|
overwrote = true;
|
|
3352
3679
|
} else {
|
|
3353
3680
|
nn = scanPlans(paths.plansDir).nextGlobal;
|
|
3354
|
-
const
|
|
3355
|
-
const baseSlug = titleHintSlug || slugify(story.id);
|
|
3356
|
-
const trackerId = config2.naming.includeTrackerId && config2.tracker.type !== "none" ? trackerIdForFilename(config2.tracker.type, story.id) : "";
|
|
3357
|
-
const trackerPart = trackerId && trackerId !== baseSlug ? `-${trackerId}` : "";
|
|
3681
|
+
const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
|
|
3358
3682
|
targetPath = path13.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.md`);
|
|
3359
3683
|
}
|
|
3360
3684
|
const body = `${input5.metadataHeader.trimEnd()}
|
|
@@ -3362,12 +3686,45 @@ function writePlanFile(input5) {
|
|
|
3362
3686
|
${input5.planBodyMarkdown.trimStart()}
|
|
3363
3687
|
`;
|
|
3364
3688
|
fs20.writeFileSync(targetPath, body, "utf8");
|
|
3365
|
-
upsertOverviewRow(featurePlanDir, story.feature, nn, path13.basename(targetPath), story);
|
|
3689
|
+
upsertOverviewRow(featurePlanDir, story.feature, nn, path13.basename(targetPath), story, false);
|
|
3690
|
+
return { planFile: targetPath, sequenceNumber: nn, overwrote };
|
|
3691
|
+
}
|
|
3692
|
+
function writePartialPlanFile(input5, featurePlanDir, paths, config2, story) {
|
|
3693
|
+
const existingPartial = findExistingPartialPlan(featurePlanDir, story.id);
|
|
3694
|
+
let targetPath;
|
|
3695
|
+
let nn;
|
|
3696
|
+
let overwrote = false;
|
|
3697
|
+
if (existingPartial) {
|
|
3698
|
+
targetPath = existingPartial.absPath;
|
|
3699
|
+
nn = existingPartial.nn;
|
|
3700
|
+
overwrote = true;
|
|
3701
|
+
} else {
|
|
3702
|
+
nn = scanPlans(paths.plansDir).nextGlobal;
|
|
3703
|
+
const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
|
|
3704
|
+
targetPath = path13.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.partial.md`);
|
|
3705
|
+
}
|
|
3706
|
+
const body = `${input5.metadataHeader.trimEnd()}
|
|
3707
|
+
|
|
3708
|
+
${input5.planBodyMarkdown.trimStart()}
|
|
3709
|
+
`;
|
|
3710
|
+
fs20.writeFileSync(targetPath, body, "utf8");
|
|
3711
|
+
upsertOverviewRow(featurePlanDir, story.feature, nn, path13.basename(targetPath), story, true);
|
|
3366
3712
|
return { planFile: targetPath, sequenceNumber: nn, overwrote };
|
|
3367
3713
|
}
|
|
3368
3714
|
function findExistingPlan(featureDir, storyId) {
|
|
3369
3715
|
if (!fs20.existsSync(featureDir)) return void 0;
|
|
3370
3716
|
const pattern = /^(\d{2,})-story-.+\.md$/;
|
|
3717
|
+
for (const entry of fs20.readdirSync(featureDir)) {
|
|
3718
|
+
if (entry.endsWith(".partial.md")) continue;
|
|
3719
|
+
const m = entry.match(pattern);
|
|
3720
|
+
if (!m) continue;
|
|
3721
|
+
if (entry.includes(storyId)) return { absPath: path13.join(featureDir, entry), nn: parseInt(m[1], 10) };
|
|
3722
|
+
}
|
|
3723
|
+
return void 0;
|
|
3724
|
+
}
|
|
3725
|
+
function findExistingPartialPlan(featureDir, storyId) {
|
|
3726
|
+
if (!fs20.existsSync(featureDir)) return void 0;
|
|
3727
|
+
const pattern = /^(\d{2,})-story-.+\.partial\.md$/;
|
|
3371
3728
|
for (const entry of fs20.readdirSync(featureDir)) {
|
|
3372
3729
|
const m = entry.match(pattern);
|
|
3373
3730
|
if (!m) continue;
|
|
@@ -3375,9 +3732,10 @@ function findExistingPlan(featureDir, storyId) {
|
|
|
3375
3732
|
}
|
|
3376
3733
|
return void 0;
|
|
3377
3734
|
}
|
|
3378
|
-
function upsertOverviewRow(featureDir, feature, nn, filename, story) {
|
|
3735
|
+
function upsertOverviewRow(featureDir, feature, nn, filename, story, isPartial) {
|
|
3379
3736
|
const overviewPath = path13.join(featureDir, "00-overview.md");
|
|
3380
|
-
const
|
|
3737
|
+
const titleCell = isPartial ? `${story.titleHint ?? story.id} (partial)` : story.titleHint ?? story.id;
|
|
3738
|
+
const row = `| ${formatSequence(nn)} | \`${filename}\` | ${titleCell} | ${story.id} | \u2014 |`;
|
|
3381
3739
|
if (!fs20.existsSync(overviewPath)) {
|
|
3382
3740
|
const content = `# ${feature} \u2014 plan overview
|
|
3383
3741
|
|
|
@@ -3423,7 +3781,15 @@ function buildMetadataHeader(args) {
|
|
|
3423
3781
|
`${args.inputTokens} in / ${args.outputTokens} out tokens`,
|
|
3424
3782
|
args.costUsd !== void 0 ? `~$${args.costUsd.toFixed(2)}` : null
|
|
3425
3783
|
].filter(Boolean);
|
|
3426
|
-
|
|
3784
|
+
const line1 = `<!-- squad-kit: ${parts.join(", ")} -->`;
|
|
3785
|
+
if (args.planStatus === "partial") {
|
|
3786
|
+
return `${line1}
|
|
3787
|
+
|
|
3788
|
+
---
|
|
3789
|
+
squad-kit-plan-status: partial
|
|
3790
|
+
---`;
|
|
3791
|
+
}
|
|
3792
|
+
return line1;
|
|
3427
3793
|
}
|
|
3428
3794
|
|
|
3429
3795
|
// src/core/last-run.ts
|
|
@@ -3561,7 +3927,19 @@ async function confirmOverwrite(story, interactive, yes) {
|
|
|
3561
3927
|
const go = await confirm6({ message: "Proceed and regenerate?", default: false });
|
|
3562
3928
|
return go;
|
|
3563
3929
|
}
|
|
3564
|
-
|
|
3930
|
+
function printPlannerRunIncompleteSummary(result, relPlanFile) {
|
|
3931
|
+
blank();
|
|
3932
|
+
warning("Planning stopped before the model reported a clean completion.");
|
|
3933
|
+
kv("saved", relPlanFile, 8);
|
|
3934
|
+
if (result.userCancelled) kv("stopped", "you chose not to continue after a limit", 8);
|
|
3935
|
+
if (result.timedOut) kv("timed out", "yes", 8);
|
|
3936
|
+
if (result.budgetExhausted) kv("read budget", "exhausted (model was asked to finalise)", 8);
|
|
3937
|
+
if (result.incompleteKind) kv("detail", result.incompleteKind, 8);
|
|
3938
|
+
blank();
|
|
3939
|
+
info("The file uses the `.partial.md` suffix and YAML `squad-kit-plan-status: partial`.");
|
|
3940
|
+
info("Raise limits in `.squad/config.yaml` (`planner.budget`, `planner.maxOutputTokens`) or re-run when ready.");
|
|
3941
|
+
}
|
|
3942
|
+
async function emitViaApi(story, paths, config2, opts) {
|
|
3565
3943
|
const planner = config2.planner;
|
|
3566
3944
|
if (!planner?.enabled) {
|
|
3567
3945
|
throw new Error(
|
|
@@ -3586,6 +3964,10 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3586
3964
|
`${planner.budget.maxFileReads} reads \xB7 ${(planner.budget.maxContextBytes / 1024).toFixed(0)} KB \xB7 ${planner.budget.maxDurationSeconds}s`
|
|
3587
3965
|
);
|
|
3588
3966
|
blank();
|
|
3967
|
+
const interactive = !opts.yes && Boolean(process.stdin.isTTY);
|
|
3968
|
+
if (interactive) {
|
|
3969
|
+
printPlannerApiCostNotice();
|
|
3970
|
+
}
|
|
3589
3971
|
const mapSpinner = spinner("building repo map\u2026");
|
|
3590
3972
|
const repoMap = buildRepoMap(paths.root);
|
|
3591
3973
|
mapSpinner.succeed(
|
|
@@ -3602,6 +3984,18 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3602
3984
|
const sessionSpinner = { current: null };
|
|
3603
3985
|
const startedAt = Date.now();
|
|
3604
3986
|
const cacheEnabled = planner.cache?.enabled ?? true;
|
|
3987
|
+
const decideOnLimit = interactive ? async (ctx) => {
|
|
3988
|
+
printPlannerLimitExplanation(ctx);
|
|
3989
|
+
const ans = await select5({
|
|
3990
|
+
message: "Continue this planning session? (Continuing sends more API requests and is billed.)",
|
|
3991
|
+
choices: [
|
|
3992
|
+
{ name: "Continue \u2014 extend limits for this run", value: "continue" },
|
|
3993
|
+
{ name: "Stop \u2014 save partial plan and exit", value: "cancel" }
|
|
3994
|
+
]
|
|
3995
|
+
});
|
|
3996
|
+
if (ans === "cancel") printPlannerLimitNextSteps();
|
|
3997
|
+
return ans;
|
|
3998
|
+
} : void 0;
|
|
3605
3999
|
const result = await runPlanner({
|
|
3606
4000
|
root: paths.root,
|
|
3607
4001
|
provider,
|
|
@@ -3610,7 +4004,9 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3610
4004
|
systemPrompt,
|
|
3611
4005
|
userPrompt,
|
|
3612
4006
|
budget,
|
|
4007
|
+
maxOutputTokens: planner.maxOutputTokens,
|
|
3613
4008
|
cacheEnabled,
|
|
4009
|
+
decideOnLimit,
|
|
3614
4010
|
onToolCall: (tc, bytes, total) => {
|
|
3615
4011
|
const p = tc.input.path ?? "<unknown>";
|
|
3616
4012
|
sessionSpinner.current?.succeed(
|
|
@@ -3636,6 +4032,7 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3636
4032
|
}
|
|
3637
4033
|
const snap = budget.snapshot();
|
|
3638
4034
|
const elapsedMs = Date.now() - startedAt;
|
|
4035
|
+
const success2 = result.finishedNormally && !result.timedOut && !result.userCancelled;
|
|
3639
4036
|
const header = buildMetadataHeader({
|
|
3640
4037
|
provider: planner.provider,
|
|
3641
4038
|
model,
|
|
@@ -3644,14 +4041,16 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3644
4041
|
inputTokens: snap.usage.inputTokens,
|
|
3645
4042
|
outputTokens: snap.usage.outputTokens,
|
|
3646
4043
|
durationMs: elapsedMs,
|
|
3647
|
-
costUsd: snap.usage.costUsd
|
|
4044
|
+
costUsd: snap.usage.costUsd,
|
|
4045
|
+
planStatus: success2 ? void 0 : "partial"
|
|
3648
4046
|
});
|
|
3649
4047
|
const { planFile, sequenceNumber, overwrote } = writePlanFile({
|
|
3650
4048
|
paths,
|
|
3651
4049
|
config: config2,
|
|
3652
4050
|
story,
|
|
3653
4051
|
planBodyMarkdown: result.planText,
|
|
3654
|
-
metadataHeader: header
|
|
4052
|
+
metadataHeader: header,
|
|
4053
|
+
partial: !success2
|
|
3655
4054
|
});
|
|
3656
4055
|
try {
|
|
3657
4056
|
await writeLastRun(paths, {
|
|
@@ -3662,9 +4061,10 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3662
4061
|
});
|
|
3663
4062
|
} catch {
|
|
3664
4063
|
}
|
|
4064
|
+
const relPlan = path15.relative(paths.root, planFile);
|
|
3665
4065
|
blank();
|
|
3666
|
-
summaryBox(" plan generated ", [
|
|
3667
|
-
{ key: "file", value:
|
|
4066
|
+
summaryBox(success2 ? " plan generated " : " plan saved (incomplete) ", [
|
|
4067
|
+
{ key: "file", value: relPlan },
|
|
3668
4068
|
{ key: "nn", value: String(sequenceNumber).padStart(2, "0") },
|
|
3669
4069
|
{ key: "model", value: `${planner.provider}/${model}` },
|
|
3670
4070
|
{ key: "reads", value: `${snap.reads} files \xB7 ${(snap.bytes / 1024).toFixed(1)} KB` },
|
|
@@ -3673,13 +4073,22 @@ async function emitViaApi(story, paths, config2, _opts) {
|
|
|
3673
4073
|
value: `${snap.usage.inputTokens} in \xB7 ${snap.usage.outputTokens} out${snap.usage.costUsd !== void 0 ? ` \u2248 $${snap.usage.costUsd.toFixed(2)}` : ""}`
|
|
3674
4074
|
},
|
|
3675
4075
|
{ key: "cache", value: formatPlannerCacheLine({ cacheEnabled, stats: result.stats }) },
|
|
4076
|
+
{
|
|
4077
|
+
key: "max out",
|
|
4078
|
+
value: `${planner.maxOutputTokens ?? DEFAULT_PLANNER_MAX_OUTPUT_TOKENS} tok/req`
|
|
4079
|
+
},
|
|
3676
4080
|
{ key: "time", value: `${Math.round(elapsedMs / 1e3)}s` },
|
|
3677
|
-
{
|
|
4081
|
+
{
|
|
4082
|
+
key: "action",
|
|
4083
|
+
value: success2 ? overwrote ? "overwrote existing plan" : "new plan" : "partial plan (review limits)"
|
|
4084
|
+
}
|
|
3678
4085
|
]);
|
|
3679
|
-
if (
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
4086
|
+
if (!success2) {
|
|
4087
|
+
printPlannerRunIncompleteSummary(result, relPlan);
|
|
4088
|
+
throw new SquadExit(2);
|
|
4089
|
+
}
|
|
4090
|
+
if (result.budgetExhausted) {
|
|
4091
|
+
info("Note: file-read budget was reached; the model finalised without further reads.");
|
|
3683
4092
|
}
|
|
3684
4093
|
blank();
|
|
3685
4094
|
info(`next \u2192 open a new agent chat and attach only ${path15.basename(planFile)}`);
|
|
@@ -3859,7 +4268,7 @@ async function runList(opts) {
|
|
|
3859
4268
|
r.id,
|
|
3860
4269
|
r.hasIntake ? "yes" : "\u2014",
|
|
3861
4270
|
r.planFile ?? theme_exports.dim("\u2014"),
|
|
3862
|
-
r.meta?.generatedBy ?? (r.planFile ? "
|
|
4271
|
+
r.meta?.generatedBy ?? (r.planFile ? theme_exports.dim("not recorded") : theme_exports.dim("\u2014"))
|
|
3863
4272
|
]);
|
|
3864
4273
|
printTable([header, ...table]);
|
|
3865
4274
|
}
|
|
@@ -5344,7 +5753,7 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
5344
5753
|
|
|
5345
5754
|
// src/cli.ts
|
|
5346
5755
|
var program = new Command();
|
|
5347
|
-
program.name("squad").description("Plan once, execute cheap. A 3-step SDD workflow CLI.").version("0.
|
|
5756
|
+
program.name("squad").description("Plan once, execute cheap. A 3-step SDD workflow CLI.").version("0.4.0");
|
|
5348
5757
|
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(
|
|
5349
5758
|
"--tracker-workspace <hostOrOrg>",
|
|
5350
5759
|
"Jira host or Azure org (non-interactive default; also written to config for jira/azure)"
|
|
@@ -5398,6 +5807,10 @@ config.command("remove-credential <section>").description('Delete credentials fo
|
|
|
5398
5807
|
)
|
|
5399
5808
|
);
|
|
5400
5809
|
program.parseAsync(process.argv).catch((err) => {
|
|
5810
|
+
if (isSquadExit(err)) {
|
|
5811
|
+
if (err.message.trim()) info(err.message);
|
|
5812
|
+
process.exit(err.exitCode);
|
|
5813
|
+
}
|
|
5401
5814
|
renderError(err);
|
|
5402
5815
|
process.exit(1);
|
|
5403
5816
|
});
|
|
@@ -5406,6 +5819,10 @@ function wrap(fn) {
|
|
|
5406
5819
|
try {
|
|
5407
5820
|
await fn(opts);
|
|
5408
5821
|
} catch (err) {
|
|
5822
|
+
if (isSquadExit(err)) {
|
|
5823
|
+
if (err.message.trim()) info(err.message);
|
|
5824
|
+
process.exit(err.exitCode);
|
|
5825
|
+
}
|
|
5409
5826
|
renderError(err);
|
|
5410
5827
|
process.exit(1);
|
|
5411
5828
|
}
|
|
@@ -5416,6 +5833,10 @@ function wrapArgs(fn) {
|
|
|
5416
5833
|
try {
|
|
5417
5834
|
await fn(...args);
|
|
5418
5835
|
} catch (err) {
|
|
5836
|
+
if (isSquadExit(err)) {
|
|
5837
|
+
if (err.message.trim()) info(err.message);
|
|
5838
|
+
process.exit(err.exitCode);
|
|
5839
|
+
}
|
|
5419
5840
|
renderError(err);
|
|
5420
5841
|
process.exit(1);
|
|
5421
5842
|
}
|