squad-kit 0.10.0 → 0.11.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 +20 -0
- package/dist/cli.js +1505 -509
- package/dist/console-ui/assets/index-BBs6jnF_.css +1 -0
- package/dist/console-ui/assets/index-n5R1pqsY.js +442 -0
- package/dist/console-ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/console-ui/assets/index-C5I4cnED.css +0 -1
- package/dist/console-ui/assets/index-Dnb4Fisq.js +0 -416
package/dist/cli.js
CHANGED
|
@@ -100,6 +100,8 @@ function spinner(text) {
|
|
|
100
100
|
return {
|
|
101
101
|
update: (t) => process.stderr.write(` \u25C6 ${t}
|
|
102
102
|
`),
|
|
103
|
+
setPrefix: () => {
|
|
104
|
+
},
|
|
103
105
|
succeed: (t) => process.stderr.write(` \u2713 ${t ?? text}
|
|
104
106
|
`),
|
|
105
107
|
fail: (t) => process.stderr.write(` \u2717 ${t ?? text}
|
|
@@ -119,6 +121,9 @@ function spinner(text) {
|
|
|
119
121
|
update: (t) => {
|
|
120
122
|
instance.text = t;
|
|
121
123
|
},
|
|
124
|
+
setPrefix: (t) => {
|
|
125
|
+
instance.prefixText = t === "" ? " " : t;
|
|
126
|
+
},
|
|
122
127
|
succeed: (t) => instance.succeed(t),
|
|
123
128
|
fail: (t) => instance.fail(t),
|
|
124
129
|
stop: () => instance.stop()
|
|
@@ -169,6 +174,77 @@ function renderError(err2) {
|
|
|
169
174
|
`);
|
|
170
175
|
}
|
|
171
176
|
|
|
177
|
+
// src/ui/stage-pipeline.ts
|
|
178
|
+
import kleur2 from "kleur";
|
|
179
|
+
var segSep = () => ` ${dim("\u2500\u2500\u25B6")} `;
|
|
180
|
+
function fmtStage(name, state) {
|
|
181
|
+
switch (state) {
|
|
182
|
+
case "pending":
|
|
183
|
+
return `${dim(`\u25CB${name}`)}`;
|
|
184
|
+
case "running":
|
|
185
|
+
return `${kleur2.cyan(`\u25CF${name}`)}${kleur2.cyan(" \u25B0\u25B0\u25B1")}`;
|
|
186
|
+
case "success":
|
|
187
|
+
return `${kleur2.green(`\u25CF${name} \u2713`)}`;
|
|
188
|
+
case "failed":
|
|
189
|
+
return `${kleur2.red(`\u25CF${name} \u2717`)}`;
|
|
190
|
+
case "skipped":
|
|
191
|
+
return `${dim(`\u25CB${name} (skipped)`)}`;
|
|
192
|
+
default:
|
|
193
|
+
return dim(`\u25CB${name}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function formatStagePipeline(line2) {
|
|
197
|
+
const sep = segSep();
|
|
198
|
+
return [fmtStage("scout", line2.scout), fmtStage("draft", line2.draft), fmtStage("validation", line2.validation)].join(
|
|
199
|
+
sep
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
function printStagePipeline(line2) {
|
|
203
|
+
process.stderr.write(` ${dim("\u2192")} ${formatStagePipeline(line2)}
|
|
204
|
+
`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/ui/budget-meter.ts
|
|
208
|
+
import kleur3 from "kleur";
|
|
209
|
+
function segmentStyle(ratio) {
|
|
210
|
+
if (ratio >= 0.9) return kleur3.red;
|
|
211
|
+
if (ratio >= 0.7) return kleur3.yellow;
|
|
212
|
+
return (s) => s;
|
|
213
|
+
}
|
|
214
|
+
function fmtRatio(n, cap) {
|
|
215
|
+
const safeCap = cap > 0 ? cap : 1;
|
|
216
|
+
const ratio = Math.min(1, n / safeCap);
|
|
217
|
+
const text = `${n}/${cap}`;
|
|
218
|
+
return { text, ratio };
|
|
219
|
+
}
|
|
220
|
+
function fmtTimeSeg(elapsedMs, capMs) {
|
|
221
|
+
const elapsedSec = Math.max(0, Math.round(elapsedMs / 1e3));
|
|
222
|
+
const capSec = capMs > 0 ? Math.round(capMs / 1e3) : 0;
|
|
223
|
+
const safeCap = capSec > 0 ? capSec : 1;
|
|
224
|
+
const ratio = capSec > 0 ? Math.min(1, elapsedSec / safeCap) : 0;
|
|
225
|
+
return { text: `${elapsedSec}/${capSec}s`, ratio };
|
|
226
|
+
}
|
|
227
|
+
function formatBudgetMeter(b) {
|
|
228
|
+
const ctxKb = Math.round(b.bytes / 1024);
|
|
229
|
+
const ctxCapKb = Math.round(b.bytesCap / 1024);
|
|
230
|
+
const reads = fmtRatio(b.reads, b.readsCap);
|
|
231
|
+
const ctx = fmtRatio(ctxKb, ctxCapKb);
|
|
232
|
+
const time = fmtTimeSeg(b.elapsedMs, b.durationMsCap);
|
|
233
|
+
const readsS = segmentStyle(reads.ratio)(`reads ${reads.text}`);
|
|
234
|
+
const ctxS = segmentStyle(ctx.ratio)(`ctx ${ctx.text} KB`);
|
|
235
|
+
const timeS = segmentStyle(time.ratio)(`time ${time.text}`);
|
|
236
|
+
return `${readsS} \xB7 ${ctxS} \xB7 ${timeS}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/ui/thinking-line.ts
|
|
240
|
+
import kleur4 from "kleur";
|
|
241
|
+
function formatThinkingLine(opts) {
|
|
242
|
+
const sec = Math.max(0, Math.round(opts.durationMs / 1e3));
|
|
243
|
+
const head = opts.running ? "thinking\u2026" : "thought";
|
|
244
|
+
const body = `${sec}s \xB7 ${opts.chars} chars`;
|
|
245
|
+
return kleur4.magenta(`${head} ${body}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
172
248
|
// src/core/cli-exit.ts
|
|
173
249
|
var SquadExit = class extends Error {
|
|
174
250
|
constructor(exitCode, message) {
|
|
@@ -303,15 +379,15 @@ function clampPlannerMaxOut(raw) {
|
|
|
303
379
|
Math.max(MIN_PLANNER_MAX_OUTPUT_TOKENS, Math.round(n))
|
|
304
380
|
);
|
|
305
381
|
}
|
|
306
|
-
function rejectSecretsInYaml(node, configFile,
|
|
382
|
+
function rejectSecretsInYaml(node, configFile, path39 = []) {
|
|
307
383
|
if (node === null || typeof node !== "object") return;
|
|
308
384
|
for (const [key, value] of Object.entries(node)) {
|
|
309
385
|
if (FORBIDDEN_KEYS.includes(key.toLowerCase())) {
|
|
310
386
|
throw new Error(
|
|
311
|
-
`Refusing to load ${configFile}: key "${[...
|
|
387
|
+
`Refusing to load ${configFile}: key "${[...path39, key].join(".")}" looks like a secret. Run \`squad config set planner\` or \`squad config set tracker\` to save it to .squad/secrets.yaml, then remove the key from config.yaml. Provider keys also work via ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY.`
|
|
312
388
|
);
|
|
313
389
|
}
|
|
314
|
-
if (typeof value === "object") rejectSecretsInYaml(value, configFile, [...
|
|
390
|
+
if (typeof value === "object") rejectSecretsInYaml(value, configFile, [...path39, key]);
|
|
315
391
|
}
|
|
316
392
|
}
|
|
317
393
|
function mergePlanner(base, override) {
|
|
@@ -837,40 +913,60 @@ function overlayTrackerEnv(base) {
|
|
|
837
913
|
|
|
838
914
|
// src/core/probes.ts
|
|
839
915
|
async function fetchProviderModelIds(provider, apiKey) {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
916
|
+
const netErr = (err2) => {
|
|
917
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
918
|
+
return { ok: false, kind: "http", status: 0, body: msg.slice(0, 500) };
|
|
919
|
+
};
|
|
920
|
+
try {
|
|
921
|
+
switch (provider) {
|
|
922
|
+
case "anthropic": {
|
|
923
|
+
const res = await fetch("https://api.anthropic.com/v1/models?limit=200", {
|
|
924
|
+
headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" }
|
|
925
|
+
});
|
|
926
|
+
if (!res.ok) {
|
|
927
|
+
return { ok: false, kind: "http", status: res.status, body: (await res.text()).slice(0, 2e3) };
|
|
928
|
+
}
|
|
929
|
+
try {
|
|
930
|
+
const body = await res.json();
|
|
931
|
+
return { ok: true, ids: new Set((body.data ?? []).map((m) => m.id)) };
|
|
932
|
+
} catch (err2) {
|
|
933
|
+
return netErr(err2);
|
|
934
|
+
}
|
|
847
935
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
936
|
+
case "openai": {
|
|
937
|
+
const res = await fetch("https://api.openai.com/v1/models", {
|
|
938
|
+
headers: { authorization: `Bearer ${apiKey}` }
|
|
939
|
+
});
|
|
940
|
+
if (!res.ok) {
|
|
941
|
+
return { ok: false, kind: "http", status: res.status, body: (await res.text()).slice(0, 2e3) };
|
|
942
|
+
}
|
|
943
|
+
try {
|
|
944
|
+
const body = await res.json();
|
|
945
|
+
return { ok: true, ids: new Set((body.data ?? []).map((m) => m.id)) };
|
|
946
|
+
} catch (err2) {
|
|
947
|
+
return netErr(err2);
|
|
948
|
+
}
|
|
857
949
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
950
|
+
case "google": {
|
|
951
|
+
const res = await fetch(
|
|
952
|
+
`https://generativelanguage.googleapis.com/v1beta/models?pageSize=200&key=${encodeURIComponent(apiKey)}`
|
|
953
|
+
);
|
|
954
|
+
if (!res.ok) {
|
|
955
|
+
return { ok: false, kind: "http", status: res.status, body: (await res.text()).slice(0, 2e3) };
|
|
956
|
+
}
|
|
957
|
+
try {
|
|
958
|
+
const body = await res.json();
|
|
959
|
+
return {
|
|
960
|
+
ok: true,
|
|
961
|
+
ids: new Set((body.models ?? []).map((m) => m.name.replace(/^models\//, "")))
|
|
962
|
+
};
|
|
963
|
+
} catch (err2) {
|
|
964
|
+
return netErr(err2);
|
|
965
|
+
}
|
|
867
966
|
}
|
|
868
|
-
const body = await res.json();
|
|
869
|
-
return {
|
|
870
|
-
ok: true,
|
|
871
|
-
ids: new Set((body.models ?? []).map((m) => m.name.replace(/^models\//, "")))
|
|
872
|
-
};
|
|
873
967
|
}
|
|
968
|
+
} catch (err2) {
|
|
969
|
+
return netErr(err2);
|
|
874
970
|
}
|
|
875
971
|
}
|
|
876
972
|
function jiraMyselfUrl(host) {
|
|
@@ -3236,8 +3332,8 @@ function truncate(s, n) {
|
|
|
3236
3332
|
}
|
|
3237
3333
|
|
|
3238
3334
|
// src/commands/new-plan.ts
|
|
3239
|
-
import
|
|
3240
|
-
import
|
|
3335
|
+
import fs32 from "fs";
|
|
3336
|
+
import path24 from "path";
|
|
3241
3337
|
import { confirm as confirm6, select as select6 } from "@inquirer/prompts";
|
|
3242
3338
|
|
|
3243
3339
|
// src/utils/clipboard.ts
|
|
@@ -3329,8 +3425,8 @@ function noCandidateReason() {
|
|
|
3329
3425
|
}
|
|
3330
3426
|
|
|
3331
3427
|
// src/planner/loop.ts
|
|
3332
|
-
import
|
|
3333
|
-
import
|
|
3428
|
+
import path20 from "path";
|
|
3429
|
+
import fs28 from "fs";
|
|
3334
3430
|
import { APICallError as APICallError3 } from "ai";
|
|
3335
3431
|
|
|
3336
3432
|
// src/planner/tools/index.ts
|
|
@@ -3849,40 +3945,80 @@ function buildPlannerToolDefinitions(opts) {
|
|
|
3849
3945
|
parameters: readParams,
|
|
3850
3946
|
execute: async (args) => {
|
|
3851
3947
|
const turn = opts.getTurn();
|
|
3948
|
+
const toolCallId = randomUUID();
|
|
3949
|
+
const startedAtMs = Date.now();
|
|
3852
3950
|
const tc = {
|
|
3853
|
-
id:
|
|
3951
|
+
id: toolCallId,
|
|
3854
3952
|
name: "read_file",
|
|
3855
3953
|
input: { path: args.path, offset: args.offset, limit: args.limit }
|
|
3856
3954
|
};
|
|
3857
|
-
const before = opts.budget.snapshot().bytes;
|
|
3858
|
-
let result = readFileTool(opts.root, opts.budget, args);
|
|
3859
|
-
let after = opts.budget.snapshot().bytes;
|
|
3860
|
-
let bytesLoaded = after - before;
|
|
3861
|
-
opts.onToolCall?.(tc, bytesLoaded, after);
|
|
3862
3955
|
opts.bus.emit({
|
|
3863
|
-
kind: "
|
|
3956
|
+
kind: "tool_call_started",
|
|
3864
3957
|
runId: opts.runId,
|
|
3865
3958
|
turn,
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3959
|
+
toolCallId,
|
|
3960
|
+
name: "read_file",
|
|
3961
|
+
input: tc.input
|
|
3869
3962
|
});
|
|
3870
|
-
|
|
3871
|
-
const
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3963
|
+
try {
|
|
3964
|
+
const before = opts.budget.snapshot().bytes;
|
|
3965
|
+
let result = readFileTool(opts.root, opts.budget, args);
|
|
3966
|
+
let after = opts.budget.snapshot().bytes;
|
|
3967
|
+
let bytesLoaded = after - before;
|
|
3968
|
+
if (readBudgetishError(result)) {
|
|
3969
|
+
const decide = opts.decideOnLimit;
|
|
3970
|
+
if (decide) {
|
|
3971
|
+
const d = await decide(opts.getLimitCtx());
|
|
3972
|
+
if (d === "cancel") {
|
|
3973
|
+
throw new PlannerUserCancelledError(opts.getAccumulatedText(), opts.runId, turn, "budget_reads", tc, result);
|
|
3974
|
+
}
|
|
3975
|
+
opts.extendSessionLimits();
|
|
3976
|
+
result = readFileTool(opts.root, opts.budget, args);
|
|
3977
|
+
after = opts.budget.snapshot().bytes;
|
|
3978
|
+
bytesLoaded = after - before;
|
|
3979
|
+
} else {
|
|
3980
|
+
opts.setBudgetExhausted(true);
|
|
3876
3981
|
}
|
|
3877
|
-
opts.extendSessionLimits();
|
|
3878
|
-
result = readFileTool(opts.root, opts.budget, args);
|
|
3879
|
-
after = opts.budget.snapshot().bytes;
|
|
3880
|
-
bytesLoaded = after - before;
|
|
3881
|
-
} else {
|
|
3882
|
-
opts.setBudgetExhausted(true);
|
|
3883
3982
|
}
|
|
3983
|
+
opts.onToolCall?.(tc, bytesLoaded, after);
|
|
3984
|
+
opts.bus.emit({
|
|
3985
|
+
kind: "tool_call",
|
|
3986
|
+
runId: opts.runId,
|
|
3987
|
+
turn,
|
|
3988
|
+
toolCall: tc,
|
|
3989
|
+
bytesLoaded,
|
|
3990
|
+
totalBytes: after
|
|
3991
|
+
});
|
|
3992
|
+
const durationMs = Date.now() - startedAtMs;
|
|
3993
|
+
opts.bus.emit({
|
|
3994
|
+
kind: "tool_call_completed",
|
|
3995
|
+
runId: opts.runId,
|
|
3996
|
+
turn,
|
|
3997
|
+
toolCallId,
|
|
3998
|
+
name: "read_file",
|
|
3999
|
+
durationMs,
|
|
4000
|
+
bytesLoaded,
|
|
4001
|
+
totalBytes: after,
|
|
4002
|
+
isError: !!result.isError,
|
|
4003
|
+
errorSnippet: result.isError ? String(result.content).slice(0, 200) : void 0
|
|
4004
|
+
});
|
|
4005
|
+
return { content: result.content, isError: result.isError };
|
|
4006
|
+
} catch (err2) {
|
|
4007
|
+
const durationMs = Date.now() - startedAtMs;
|
|
4008
|
+
opts.bus.emit({
|
|
4009
|
+
kind: "tool_call_completed",
|
|
4010
|
+
runId: opts.runId,
|
|
4011
|
+
turn,
|
|
4012
|
+
toolCallId,
|
|
4013
|
+
name: "read_file",
|
|
4014
|
+
durationMs,
|
|
4015
|
+
bytesLoaded: 0,
|
|
4016
|
+
totalBytes: opts.budget.snapshot().bytes,
|
|
4017
|
+
isError: true,
|
|
4018
|
+
errorSnippet: err2 instanceof Error ? err2.message.slice(0, 200) : String(err2).slice(0, 200)
|
|
4019
|
+
});
|
|
4020
|
+
throw err2;
|
|
3884
4021
|
}
|
|
3885
|
-
return { content: result.content, isError: result.isError };
|
|
3886
4022
|
}
|
|
3887
4023
|
});
|
|
3888
4024
|
if (enabled.grep) {
|
|
@@ -3892,30 +4028,70 @@ function buildPlannerToolDefinitions(opts) {
|
|
|
3892
4028
|
parameters: grepSchema,
|
|
3893
4029
|
execute: async (args) => {
|
|
3894
4030
|
const turn = opts.getTurn();
|
|
4031
|
+
const toolCallId = randomUUID();
|
|
4032
|
+
const startedAtMs = Date.now();
|
|
3895
4033
|
const tc = {
|
|
3896
|
-
id:
|
|
4034
|
+
id: toolCallId,
|
|
3897
4035
|
name: GREP_TOOL_NAME,
|
|
3898
4036
|
input: { ...args }
|
|
3899
4037
|
};
|
|
3900
|
-
const before = opts.budget.snapshot().bytes;
|
|
3901
|
-
const out = runGrep(opts.root, opts.budget, {
|
|
3902
|
-
pattern: args.pattern,
|
|
3903
|
-
regex: args.regex ?? false,
|
|
3904
|
-
path: args.path,
|
|
3905
|
-
caseInsensitive: args.caseInsensitive ?? false
|
|
3906
|
-
});
|
|
3907
|
-
const after = opts.budget.snapshot().bytes;
|
|
3908
|
-
const bytesLoaded = after - before;
|
|
3909
|
-
opts.onToolCall?.(tc, bytesLoaded, after);
|
|
3910
4038
|
opts.bus.emit({
|
|
3911
|
-
kind: "
|
|
4039
|
+
kind: "tool_call_started",
|
|
3912
4040
|
runId: opts.runId,
|
|
3913
4041
|
turn,
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
4042
|
+
toolCallId,
|
|
4043
|
+
name: GREP_TOOL_NAME,
|
|
4044
|
+
input: tc.input
|
|
3917
4045
|
});
|
|
3918
|
-
|
|
4046
|
+
try {
|
|
4047
|
+
const before = opts.budget.snapshot().bytes;
|
|
4048
|
+
const out = runGrep(opts.root, opts.budget, {
|
|
4049
|
+
pattern: args.pattern,
|
|
4050
|
+
regex: args.regex ?? false,
|
|
4051
|
+
path: args.path,
|
|
4052
|
+
caseInsensitive: args.caseInsensitive ?? false
|
|
4053
|
+
});
|
|
4054
|
+
const after = opts.budget.snapshot().bytes;
|
|
4055
|
+
const bytesLoaded = after - before;
|
|
4056
|
+
opts.onToolCall?.(tc, bytesLoaded, after);
|
|
4057
|
+
opts.bus.emit({
|
|
4058
|
+
kind: "tool_call",
|
|
4059
|
+
runId: opts.runId,
|
|
4060
|
+
turn,
|
|
4061
|
+
toolCall: tc,
|
|
4062
|
+
bytesLoaded,
|
|
4063
|
+
totalBytes: after
|
|
4064
|
+
});
|
|
4065
|
+
const durationMs = Date.now() - startedAtMs;
|
|
4066
|
+
opts.bus.emit({
|
|
4067
|
+
kind: "tool_call_completed",
|
|
4068
|
+
runId: opts.runId,
|
|
4069
|
+
turn,
|
|
4070
|
+
toolCallId,
|
|
4071
|
+
name: GREP_TOOL_NAME,
|
|
4072
|
+
durationMs,
|
|
4073
|
+
bytesLoaded,
|
|
4074
|
+
totalBytes: after,
|
|
4075
|
+
isError: !!out.isError,
|
|
4076
|
+
errorSnippet: out.isError ? String(out.content).slice(0, 200) : void 0
|
|
4077
|
+
});
|
|
4078
|
+
return { content: out.content, isError: out.isError };
|
|
4079
|
+
} catch (err2) {
|
|
4080
|
+
const durationMs = Date.now() - startedAtMs;
|
|
4081
|
+
opts.bus.emit({
|
|
4082
|
+
kind: "tool_call_completed",
|
|
4083
|
+
runId: opts.runId,
|
|
4084
|
+
turn,
|
|
4085
|
+
toolCallId,
|
|
4086
|
+
name: GREP_TOOL_NAME,
|
|
4087
|
+
durationMs,
|
|
4088
|
+
bytesLoaded: 0,
|
|
4089
|
+
totalBytes: opts.budget.snapshot().bytes,
|
|
4090
|
+
isError: true,
|
|
4091
|
+
errorSnippet: err2 instanceof Error ? err2.message.slice(0, 200) : String(err2).slice(0, 200)
|
|
4092
|
+
});
|
|
4093
|
+
throw err2;
|
|
4094
|
+
}
|
|
3919
4095
|
}
|
|
3920
4096
|
});
|
|
3921
4097
|
}
|
|
@@ -3926,24 +4102,65 @@ function buildPlannerToolDefinitions(opts) {
|
|
|
3926
4102
|
parameters: listDirSchema,
|
|
3927
4103
|
execute: async (args) => {
|
|
3928
4104
|
const turn = opts.getTurn();
|
|
4105
|
+
const toolCallId = randomUUID();
|
|
4106
|
+
const startedAtMs = Date.now();
|
|
3929
4107
|
const tc = {
|
|
3930
|
-
id:
|
|
4108
|
+
id: toolCallId,
|
|
3931
4109
|
name: LIST_DIR_TOOL_NAME,
|
|
3932
4110
|
input: { path: args.path }
|
|
3933
4111
|
};
|
|
3934
|
-
const before = opts.budget.snapshot().bytes;
|
|
3935
|
-
const out = runListDir(opts.root, opts.budget, args.path);
|
|
3936
|
-
const after = opts.budget.snapshot().bytes;
|
|
3937
|
-
opts.onToolCall?.(tc, after - before, after);
|
|
3938
4112
|
opts.bus.emit({
|
|
3939
|
-
kind: "
|
|
4113
|
+
kind: "tool_call_started",
|
|
3940
4114
|
runId: opts.runId,
|
|
3941
4115
|
turn,
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
4116
|
+
toolCallId,
|
|
4117
|
+
name: LIST_DIR_TOOL_NAME,
|
|
4118
|
+
input: tc.input
|
|
3945
4119
|
});
|
|
3946
|
-
|
|
4120
|
+
try {
|
|
4121
|
+
const before = opts.budget.snapshot().bytes;
|
|
4122
|
+
const out = runListDir(opts.root, opts.budget, args.path);
|
|
4123
|
+
const after = opts.budget.snapshot().bytes;
|
|
4124
|
+
const bytesLoaded = after - before;
|
|
4125
|
+
opts.onToolCall?.(tc, bytesLoaded, after);
|
|
4126
|
+
opts.bus.emit({
|
|
4127
|
+
kind: "tool_call",
|
|
4128
|
+
runId: opts.runId,
|
|
4129
|
+
turn,
|
|
4130
|
+
toolCall: tc,
|
|
4131
|
+
bytesLoaded,
|
|
4132
|
+
totalBytes: after
|
|
4133
|
+
});
|
|
4134
|
+
const durationMs = Date.now() - startedAtMs;
|
|
4135
|
+
opts.bus.emit({
|
|
4136
|
+
kind: "tool_call_completed",
|
|
4137
|
+
runId: opts.runId,
|
|
4138
|
+
turn,
|
|
4139
|
+
toolCallId,
|
|
4140
|
+
name: LIST_DIR_TOOL_NAME,
|
|
4141
|
+
durationMs,
|
|
4142
|
+
bytesLoaded,
|
|
4143
|
+
totalBytes: after,
|
|
4144
|
+
isError: !!out.isError,
|
|
4145
|
+
errorSnippet: out.isError ? String(out.content).slice(0, 200) : void 0
|
|
4146
|
+
});
|
|
4147
|
+
return { content: out.content, isError: out.isError };
|
|
4148
|
+
} catch (err2) {
|
|
4149
|
+
const durationMs = Date.now() - startedAtMs;
|
|
4150
|
+
opts.bus.emit({
|
|
4151
|
+
kind: "tool_call_completed",
|
|
4152
|
+
runId: opts.runId,
|
|
4153
|
+
turn,
|
|
4154
|
+
toolCallId,
|
|
4155
|
+
name: LIST_DIR_TOOL_NAME,
|
|
4156
|
+
durationMs,
|
|
4157
|
+
bytesLoaded: 0,
|
|
4158
|
+
totalBytes: opts.budget.snapshot().bytes,
|
|
4159
|
+
isError: true,
|
|
4160
|
+
errorSnippet: err2 instanceof Error ? err2.message.slice(0, 200) : String(err2).slice(0, 200)
|
|
4161
|
+
});
|
|
4162
|
+
throw err2;
|
|
4163
|
+
}
|
|
3947
4164
|
}
|
|
3948
4165
|
});
|
|
3949
4166
|
}
|
|
@@ -4052,34 +4269,280 @@ function rateLimitMessage(err2) {
|
|
|
4052
4269
|
}
|
|
4053
4270
|
|
|
4054
4271
|
// src/core/runs.ts
|
|
4055
|
-
import
|
|
4056
|
-
import
|
|
4272
|
+
import fs26 from "fs/promises";
|
|
4273
|
+
import path18 from "path";
|
|
4057
4274
|
import { randomBytes } from "crypto";
|
|
4058
|
-
|
|
4275
|
+
|
|
4276
|
+
// src/core/run-retention.ts
|
|
4277
|
+
var RUN_HISTORY_RING_SIZE = 20;
|
|
4278
|
+
var RUN_EVENT_JSONL_UNCOMPRESSED_HEAD = 5;
|
|
4279
|
+
|
|
4280
|
+
// src/core/run-events-store.ts
|
|
4281
|
+
import fs25, { createWriteStream } from "fs";
|
|
4282
|
+
import fsPromises from "fs/promises";
|
|
4283
|
+
import path17 from "path";
|
|
4284
|
+
import readline from "readline";
|
|
4285
|
+
import { pipeline } from "stream/promises";
|
|
4286
|
+
import zlib from "zlib";
|
|
4287
|
+
var SUFFIX_JSONL = ".events.jsonl";
|
|
4288
|
+
var SUFFIX_GZ = ".events.jsonl.gz";
|
|
4289
|
+
function redactPlannerEventForDisk(e) {
|
|
4290
|
+
if (e.kind === "thinking_delta") return { ...e, delta: "" };
|
|
4291
|
+
return e;
|
|
4292
|
+
}
|
|
4293
|
+
function runsDir(paths) {
|
|
4294
|
+
return path17.join(paths.squadDir, "runs");
|
|
4295
|
+
}
|
|
4296
|
+
function logAppendErrorOnce(err2) {
|
|
4297
|
+
console.error("[run-events-store] append failed:", err2);
|
|
4298
|
+
}
|
|
4299
|
+
var RunEventsStoreImpl = class {
|
|
4300
|
+
stream;
|
|
4301
|
+
bytes = 0;
|
|
4302
|
+
closed = false;
|
|
4303
|
+
loggedError = false;
|
|
4304
|
+
file;
|
|
4305
|
+
constructor(file) {
|
|
4306
|
+
this.file = file;
|
|
4307
|
+
this.stream = createWriteStream(file, { flags: "a" });
|
|
4308
|
+
this.stream.on("error", (err2) => {
|
|
4309
|
+
if (!this.loggedError) {
|
|
4310
|
+
this.loggedError = true;
|
|
4311
|
+
logAppendErrorOnce(err2);
|
|
4312
|
+
}
|
|
4313
|
+
});
|
|
4314
|
+
}
|
|
4315
|
+
bytesWritten() {
|
|
4316
|
+
return this.bytes;
|
|
4317
|
+
}
|
|
4318
|
+
async append(e) {
|
|
4319
|
+
if (!this.stream || this.closed) return;
|
|
4320
|
+
const line2 = JSON.stringify(redactPlannerEventForDisk(e)) + "\n";
|
|
4321
|
+
this.bytes += Buffer.byteLength(line2, "utf8");
|
|
4322
|
+
await new Promise((resolve2) => {
|
|
4323
|
+
const s = this.stream;
|
|
4324
|
+
if (!s) {
|
|
4325
|
+
resolve2();
|
|
4326
|
+
return;
|
|
4327
|
+
}
|
|
4328
|
+
s.write(line2, (err2) => {
|
|
4329
|
+
if (err2 && !this.loggedError) {
|
|
4330
|
+
this.loggedError = true;
|
|
4331
|
+
logAppendErrorOnce(err2);
|
|
4332
|
+
}
|
|
4333
|
+
resolve2();
|
|
4334
|
+
});
|
|
4335
|
+
});
|
|
4336
|
+
}
|
|
4337
|
+
async close() {
|
|
4338
|
+
if (this.closed) return;
|
|
4339
|
+
this.closed = true;
|
|
4340
|
+
const s = this.stream;
|
|
4341
|
+
this.stream = null;
|
|
4342
|
+
if (!s) return;
|
|
4343
|
+
await new Promise((resolve2) => {
|
|
4344
|
+
s.end(() => resolve2());
|
|
4345
|
+
s.on("error", () => resolve2());
|
|
4346
|
+
});
|
|
4347
|
+
}
|
|
4348
|
+
};
|
|
4349
|
+
function createRunEventsStore(paths, runId) {
|
|
4350
|
+
const dir = runsDir(paths);
|
|
4351
|
+
fs25.mkdirSync(dir, { recursive: true });
|
|
4352
|
+
const file = path17.join(dir, `${runId}${SUFFIX_JSONL}`);
|
|
4353
|
+
return new RunEventsStoreImpl(file);
|
|
4354
|
+
}
|
|
4355
|
+
var RunEventsReaderImpl = class {
|
|
4356
|
+
filePath;
|
|
4357
|
+
useGunzip;
|
|
4358
|
+
_count = null;
|
|
4359
|
+
constructor(filePath, useGunzip) {
|
|
4360
|
+
this.filePath = filePath;
|
|
4361
|
+
this.useGunzip = useGunzip;
|
|
4362
|
+
}
|
|
4363
|
+
async count() {
|
|
4364
|
+
if (this._count !== null) return this._count;
|
|
4365
|
+
let n = 0;
|
|
4366
|
+
for await (const _ of this.iterate()) {
|
|
4367
|
+
n++;
|
|
4368
|
+
}
|
|
4369
|
+
this._count = n;
|
|
4370
|
+
return n;
|
|
4371
|
+
}
|
|
4372
|
+
async *iterate(opts) {
|
|
4373
|
+
const fromIndex = Math.max(0, opts?.fromIndex ?? 0);
|
|
4374
|
+
const limit = opts?.limit ?? Number.POSITIVE_INFINITY;
|
|
4375
|
+
let logical = 0;
|
|
4376
|
+
let yielded = 0;
|
|
4377
|
+
const stream = fs25.createReadStream(this.filePath);
|
|
4378
|
+
const input5 = this.useGunzip ? stream.pipe(zlib.createGunzip()) : stream;
|
|
4379
|
+
const rl = readline.createInterface({ input: input5, crlfDelay: Infinity });
|
|
4380
|
+
try {
|
|
4381
|
+
for await (const line2 of rl) {
|
|
4382
|
+
const trimmed = line2.trim();
|
|
4383
|
+
if (!trimmed) continue;
|
|
4384
|
+
let ev;
|
|
4385
|
+
try {
|
|
4386
|
+
ev = JSON.parse(trimmed);
|
|
4387
|
+
} catch {
|
|
4388
|
+
continue;
|
|
4389
|
+
}
|
|
4390
|
+
if (logical >= fromIndex && yielded < limit) {
|
|
4391
|
+
yield ev;
|
|
4392
|
+
yielded++;
|
|
4393
|
+
if (yielded >= limit) break;
|
|
4394
|
+
}
|
|
4395
|
+
logical++;
|
|
4396
|
+
}
|
|
4397
|
+
} finally {
|
|
4398
|
+
rl.close();
|
|
4399
|
+
stream.destroy();
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
};
|
|
4403
|
+
async function openRunEventsReader(paths, runId) {
|
|
4404
|
+
const dir = runsDir(paths);
|
|
4405
|
+
const jsonl = path17.join(dir, `${runId}${SUFFIX_JSONL}`);
|
|
4406
|
+
const gz = path17.join(dir, `${runId}${SUFFIX_GZ}`);
|
|
4407
|
+
let jsonlStat = null;
|
|
4408
|
+
let gzStat = null;
|
|
4409
|
+
try {
|
|
4410
|
+
jsonlStat = await fsPromises.stat(jsonl).then((s) => ({ mtimeMs: s.mtimeMs }));
|
|
4411
|
+
} catch {
|
|
4412
|
+
jsonlStat = null;
|
|
4413
|
+
}
|
|
4414
|
+
try {
|
|
4415
|
+
gzStat = await fsPromises.stat(gz).then((s) => ({ mtimeMs: s.mtimeMs }));
|
|
4416
|
+
} catch {
|
|
4417
|
+
gzStat = null;
|
|
4418
|
+
}
|
|
4419
|
+
if (gzStat && jsonlStat) return new RunEventsReaderImpl(gz, true);
|
|
4420
|
+
if (gzStat) return new RunEventsReaderImpl(gz, true);
|
|
4421
|
+
if (jsonlStat) return new RunEventsReaderImpl(jsonl, false);
|
|
4422
|
+
return null;
|
|
4423
|
+
}
|
|
4424
|
+
var rotateChain = Promise.resolve();
|
|
4425
|
+
function collectEventLogs(dir, names) {
|
|
4426
|
+
const byRun = /* @__PURE__ */ new Map();
|
|
4427
|
+
for (const name of names) {
|
|
4428
|
+
let runId = null;
|
|
4429
|
+
let kind = null;
|
|
4430
|
+
if (name.endsWith(SUFFIX_GZ)) {
|
|
4431
|
+
runId = name.slice(0, -SUFFIX_GZ.length);
|
|
4432
|
+
kind = "gz";
|
|
4433
|
+
} else if (name.endsWith(SUFFIX_JSONL)) {
|
|
4434
|
+
runId = name.slice(0, -SUFFIX_JSONL.length);
|
|
4435
|
+
kind = "jsonl";
|
|
4436
|
+
}
|
|
4437
|
+
if (!runId || !kind) continue;
|
|
4438
|
+
const full = path17.join(dir, name);
|
|
4439
|
+
let st;
|
|
4440
|
+
try {
|
|
4441
|
+
st = { mtimeMs: fs25.statSync(full).mtimeMs };
|
|
4442
|
+
} catch {
|
|
4443
|
+
continue;
|
|
4444
|
+
}
|
|
4445
|
+
const cur = byRun.get(runId) ?? { mtimeMs: 0, jsonlPath: null, gzPath: null };
|
|
4446
|
+
cur.mtimeMs = Math.max(cur.mtimeMs, st.mtimeMs);
|
|
4447
|
+
if (kind === "jsonl") cur.jsonlPath = full;
|
|
4448
|
+
else cur.gzPath = full;
|
|
4449
|
+
byRun.set(runId, cur);
|
|
4450
|
+
}
|
|
4451
|
+
const out = [];
|
|
4452
|
+
for (const [runId, v] of byRun) {
|
|
4453
|
+
out.push({ runId, mtimeMs: v.mtimeMs, jsonlPath: v.jsonlPath, gzPath: v.gzPath });
|
|
4454
|
+
}
|
|
4455
|
+
out.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
4456
|
+
return out;
|
|
4457
|
+
}
|
|
4458
|
+
async function fsyncFile(filePath) {
|
|
4459
|
+
const fh = await fsPromises.open(filePath, "r");
|
|
4460
|
+
try {
|
|
4461
|
+
await fh.sync();
|
|
4462
|
+
} finally {
|
|
4463
|
+
await fh.close();
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
async function gzipAtomic(jsonlPath, gzPath) {
|
|
4467
|
+
const gzTmp = `${gzPath}.tmp`;
|
|
4468
|
+
await fsPromises.rm(gzTmp, { force: true });
|
|
4469
|
+
await pipeline(fs25.createReadStream(jsonlPath), zlib.createGzip(), fs25.createWriteStream(gzTmp));
|
|
4470
|
+
await fsyncFile(gzTmp);
|
|
4471
|
+
await fsPromises.rename(gzTmp, gzPath);
|
|
4472
|
+
await fsPromises.unlink(jsonlPath);
|
|
4473
|
+
}
|
|
4474
|
+
async function rotateRunEventsUnlocked(paths) {
|
|
4475
|
+
const dir = runsDir(paths);
|
|
4476
|
+
let names;
|
|
4477
|
+
try {
|
|
4478
|
+
names = await fsPromises.readdir(dir);
|
|
4479
|
+
} catch {
|
|
4480
|
+
return;
|
|
4481
|
+
}
|
|
4482
|
+
for (const name of names) {
|
|
4483
|
+
if (name.endsWith(`${SUFFIX_GZ}.tmp`)) {
|
|
4484
|
+
await fsPromises.rm(path17.join(dir, name), { force: true });
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
names = await fsPromises.readdir(dir);
|
|
4488
|
+
const ordered = collectEventLogs(dir, names);
|
|
4489
|
+
for (let i = RUN_HISTORY_RING_SIZE; i < ordered.length; i++) {
|
|
4490
|
+
const e = ordered[i];
|
|
4491
|
+
const summary = path17.join(dir, `${e.runId}.json`);
|
|
4492
|
+
await fsPromises.rm(summary, { force: true });
|
|
4493
|
+
if (e.jsonlPath) await fsPromises.rm(e.jsonlPath, { force: true });
|
|
4494
|
+
if (e.gzPath) await fsPromises.rm(e.gzPath, { force: true });
|
|
4495
|
+
}
|
|
4496
|
+
const keep = ordered.slice(0, RUN_HISTORY_RING_SIZE);
|
|
4497
|
+
for (let i = RUN_EVENT_JSONL_UNCOMPRESSED_HEAD; i < keep.length; i++) {
|
|
4498
|
+
const e = keep[i];
|
|
4499
|
+
const jsonl = e.jsonlPath;
|
|
4500
|
+
if (!jsonl) continue;
|
|
4501
|
+
const gz = path17.join(dir, `${e.runId}${SUFFIX_GZ}`);
|
|
4502
|
+
await gzipAtomic(jsonl, gz);
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
function rotateRunEvents(paths) {
|
|
4506
|
+
rotateChain = rotateChain.then(() => rotateRunEventsUnlocked(paths)).catch(() => {
|
|
4507
|
+
});
|
|
4508
|
+
return rotateChain;
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
// src/core/runs.ts
|
|
4059
4512
|
function newRunId() {
|
|
4060
4513
|
const ts = Date.now().toString(36).padStart(9, "0");
|
|
4061
4514
|
const rnd = randomBytes(8).toString("hex");
|
|
4062
4515
|
return `${ts}-${rnd}`;
|
|
4063
4516
|
}
|
|
4064
|
-
async function
|
|
4065
|
-
const dir =
|
|
4066
|
-
await
|
|
4067
|
-
|
|
4068
|
-
await fs25.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
|
|
4069
|
-
await pruneOldRuns(dir);
|
|
4070
|
-
}
|
|
4071
|
-
async function pruneOldRuns(dir) {
|
|
4072
|
-
const entries = (await fs25.readdir(dir)).filter((n) => n.endsWith(".json"));
|
|
4073
|
-
if (entries.length <= RING_SIZE) return;
|
|
4517
|
+
async function pruneOldRuns(paths) {
|
|
4518
|
+
const dir = path18.join(paths.squadDir, "runs");
|
|
4519
|
+
const entries = (await fs26.readdir(dir)).filter((n) => n.endsWith(".json"));
|
|
4520
|
+
if (entries.length <= RUN_HISTORY_RING_SIZE) return;
|
|
4074
4521
|
entries.sort();
|
|
4075
|
-
const drop = entries.slice(0, entries.length -
|
|
4076
|
-
await Promise.all(
|
|
4522
|
+
const drop = entries.slice(0, entries.length - RUN_HISTORY_RING_SIZE);
|
|
4523
|
+
await Promise.all(
|
|
4524
|
+
drop.map(async (n) => {
|
|
4525
|
+
const runPath = path18.join(dir, n);
|
|
4526
|
+
const runId = n.replace(/\.json$/u, "");
|
|
4527
|
+
await fs26.rm(runPath, { force: true });
|
|
4528
|
+
await fs26.rm(path18.join(dir, `${runId}.events.jsonl`), { force: true });
|
|
4529
|
+
await fs26.rm(path18.join(dir, `${runId}.events.jsonl.gz`), { force: true });
|
|
4530
|
+
})
|
|
4531
|
+
);
|
|
4532
|
+
}
|
|
4533
|
+
async function appendRun(paths, rec) {
|
|
4534
|
+
const dir = path18.join(paths.squadDir, "runs");
|
|
4535
|
+
await fs26.mkdir(dir, { recursive: true });
|
|
4536
|
+
const file = path18.join(dir, `${rec.runId}.json`);
|
|
4537
|
+
await fs26.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
|
|
4538
|
+
await pruneOldRuns(paths);
|
|
4539
|
+
await rotateRunEvents(paths);
|
|
4077
4540
|
}
|
|
4078
4541
|
async function listRuns(paths) {
|
|
4079
|
-
const dir =
|
|
4542
|
+
const dir = path18.join(paths.squadDir, "runs");
|
|
4080
4543
|
let entries;
|
|
4081
4544
|
try {
|
|
4082
|
-
entries = await
|
|
4545
|
+
entries = await fs26.readdir(dir);
|
|
4083
4546
|
} catch {
|
|
4084
4547
|
return [];
|
|
4085
4548
|
}
|
|
@@ -4087,7 +4550,7 @@ async function listRuns(paths) {
|
|
|
4087
4550
|
const out = [];
|
|
4088
4551
|
for (const f of files) {
|
|
4089
4552
|
try {
|
|
4090
|
-
const raw = await
|
|
4553
|
+
const raw = await fs26.readFile(path18.join(dir, f), "utf8");
|
|
4091
4554
|
const parsed = JSON.parse(raw);
|
|
4092
4555
|
if (parsed.version === 1) out.push(parsed);
|
|
4093
4556
|
} catch {
|
|
@@ -4795,6 +5258,11 @@ var VercelRuntime = class {
|
|
|
4795
5258
|
|
|
4796
5259
|
// src/planner/events.ts
|
|
4797
5260
|
var PlannerEventBus = class {
|
|
5261
|
+
/**
|
|
5262
|
+
* When `runPlanner` persists JSONL timelines, callers must await this once after emitting
|
|
5263
|
+
* terminal bus events (`done`) so streams flush before process exit.
|
|
5264
|
+
*/
|
|
5265
|
+
finalizeEventPersistence;
|
|
4798
5266
|
listeners = /* @__PURE__ */ new Set();
|
|
4799
5267
|
emit(e) {
|
|
4800
5268
|
for (const fn of this.listeners) {
|
|
@@ -4871,8 +5339,8 @@ async function runScout(input5) {
|
|
|
4871
5339
|
}
|
|
4872
5340
|
|
|
4873
5341
|
// src/planner/validation.ts
|
|
4874
|
-
import
|
|
4875
|
-
import
|
|
5342
|
+
import fs27 from "fs";
|
|
5343
|
+
import path19 from "path";
|
|
4876
5344
|
var CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|md|json|ya?ml|css|html|py|go|rs|cs|java|kt|kts|swift|rb|php|vue|svelte)$/i;
|
|
4877
5345
|
function summariseIssuesByKind(issues) {
|
|
4878
5346
|
const out = {};
|
|
@@ -4942,21 +5410,21 @@ function trigramSimilarity(a, b) {
|
|
|
4942
5410
|
return inter / Math.sqrt(A.size * B.size);
|
|
4943
5411
|
}
|
|
4944
5412
|
function suggestPaths(root, missingRel, max = 3) {
|
|
4945
|
-
const targetBase =
|
|
5413
|
+
const targetBase = path19.basename(missingRel).toLowerCase();
|
|
4946
5414
|
const ig = buildRepoIgnore(root);
|
|
4947
5415
|
const candidates = [];
|
|
4948
5416
|
const walk2 = (dir) => {
|
|
4949
5417
|
if (candidates.length >= 2e3) return;
|
|
4950
5418
|
let ents;
|
|
4951
5419
|
try {
|
|
4952
|
-
ents =
|
|
5420
|
+
ents = fs27.readdirSync(dir, { withFileTypes: true });
|
|
4953
5421
|
} catch {
|
|
4954
5422
|
return;
|
|
4955
5423
|
}
|
|
4956
5424
|
for (const ent of ents) {
|
|
4957
5425
|
if (candidates.length >= 2e3) return;
|
|
4958
|
-
const abs =
|
|
4959
|
-
const rel =
|
|
5426
|
+
const abs = path19.join(dir, ent.name);
|
|
5427
|
+
const rel = path19.relative(root, abs).split(path19.sep).join("/");
|
|
4960
5428
|
if (!rel || ig.ignores(rel) || ent.isDirectory() && ig.ignores(`${rel}/`)) continue;
|
|
4961
5429
|
if (ent.isDirectory()) walk2(abs);
|
|
4962
5430
|
else if (ent.isFile()) candidates.push(rel);
|
|
@@ -4965,7 +5433,7 @@ function suggestPaths(root, missingRel, max = 3) {
|
|
|
4965
5433
|
walk2(root);
|
|
4966
5434
|
const rows = [];
|
|
4967
5435
|
for (const rel of candidates) {
|
|
4968
|
-
const base =
|
|
5436
|
+
const base = path19.basename(rel).toLowerCase();
|
|
4969
5437
|
let score = 0;
|
|
4970
5438
|
if (base === targetBase) score = 1e3;
|
|
4971
5439
|
else if (base.startsWith(targetBase) || targetBase.startsWith(base)) score = 500;
|
|
@@ -5043,12 +5511,12 @@ function validatePlan(input5) {
|
|
|
5043
5511
|
const cache = /* @__PURE__ */ new Map();
|
|
5044
5512
|
const readCached = (rel) => {
|
|
5045
5513
|
if (cache.has(rel)) return cache.get(rel);
|
|
5046
|
-
const resolved =
|
|
5047
|
-
const rc =
|
|
5048
|
-
if (rc.startsWith("..") ||
|
|
5049
|
-
if (!
|
|
5514
|
+
const resolved = path19.resolve(input5.root, rel);
|
|
5515
|
+
const rc = path19.relative(input5.root, resolved);
|
|
5516
|
+
if (rc.startsWith("..") || path19.isAbsolute(rc)) return void 0;
|
|
5517
|
+
if (!fs27.existsSync(resolved) || !fs27.statSync(resolved).isFile()) return void 0;
|
|
5050
5518
|
try {
|
|
5051
|
-
const buf =
|
|
5519
|
+
const buf = fs27.readFileSync(resolved);
|
|
5052
5520
|
const isBinary = buf.includes(0);
|
|
5053
5521
|
const text = isBinary ? "" : buf.toString("utf8");
|
|
5054
5522
|
const lines = isBinary ? 0 : text.split(/\r?\n/).length;
|
|
@@ -5062,10 +5530,10 @@ function validatePlan(input5) {
|
|
|
5062
5530
|
const pathClaims = mergePathClaims(input5.planText);
|
|
5063
5531
|
for (const c of pathClaims) {
|
|
5064
5532
|
if (c.isCreate) continue;
|
|
5065
|
-
const resolved =
|
|
5066
|
-
const rel =
|
|
5067
|
-
if (rel.startsWith("..") ||
|
|
5068
|
-
if (!
|
|
5533
|
+
const resolved = path19.resolve(input5.root, c.path);
|
|
5534
|
+
const rel = path19.relative(input5.root, resolved);
|
|
5535
|
+
if (rel.startsWith("..") || path19.isAbsolute(rel)) continue;
|
|
5536
|
+
if (!fs27.existsSync(resolved)) {
|
|
5069
5537
|
const suggestions = suggestPaths(input5.root, c.path);
|
|
5070
5538
|
issues.push({
|
|
5071
5539
|
severity: "warning",
|
|
@@ -5232,9 +5700,9 @@ ${result.content}
|
|
|
5232
5700
|
}
|
|
5233
5701
|
continue;
|
|
5234
5702
|
}
|
|
5235
|
-
const resolved =
|
|
5236
|
-
const rc =
|
|
5237
|
-
if (rc.startsWith("..") ||
|
|
5703
|
+
const resolved = path20.resolve(root, cleaned);
|
|
5704
|
+
const rc = path20.relative(root, resolved);
|
|
5705
|
+
if (rc.startsWith("..") || path20.isAbsolute(rc)) {
|
|
5238
5706
|
bus.emit({
|
|
5239
5707
|
kind: "validation_issue",
|
|
5240
5708
|
runId,
|
|
@@ -5245,7 +5713,7 @@ ${result.content}
|
|
|
5245
5713
|
});
|
|
5246
5714
|
continue;
|
|
5247
5715
|
}
|
|
5248
|
-
if (!
|
|
5716
|
+
if (!fs28.existsSync(resolved)) {
|
|
5249
5717
|
bus.emit({
|
|
5250
5718
|
kind: "validation_issue",
|
|
5251
5719
|
runId,
|
|
@@ -5256,7 +5724,7 @@ ${result.content}
|
|
|
5256
5724
|
});
|
|
5257
5725
|
continue;
|
|
5258
5726
|
}
|
|
5259
|
-
const st =
|
|
5727
|
+
const st = fs28.statSync(resolved);
|
|
5260
5728
|
if (!st.isFile()) continue;
|
|
5261
5729
|
const n = Math.min(st.size, MAX_BYTES_PER_FULL_READ);
|
|
5262
5730
|
const cap = budget.canRead(n);
|
|
@@ -5270,7 +5738,7 @@ _(skipped: ${cap.reason})_
|
|
|
5270
5738
|
}
|
|
5271
5739
|
let buf;
|
|
5272
5740
|
try {
|
|
5273
|
-
buf =
|
|
5741
|
+
buf = fs28.readFileSync(resolved);
|
|
5274
5742
|
} catch {
|
|
5275
5743
|
continue;
|
|
5276
5744
|
}
|
|
@@ -5386,6 +5854,19 @@ async function runDraftStage(input5) {
|
|
|
5386
5854
|
} : void 0
|
|
5387
5855
|
});
|
|
5388
5856
|
const incompleteKind = draft.incompleteKind;
|
|
5857
|
+
const u = input5.budget.snapshot().usage;
|
|
5858
|
+
const cacheRead = u.cacheReadTokens ?? 0;
|
|
5859
|
+
const cacheCreate = u.cacheCreationTokens ?? 0;
|
|
5860
|
+
const totalIn = u.inputTokens + cacheRead;
|
|
5861
|
+
const cacheHitRatio = totalIn === 0 ? 0 : Math.round(cacheRead / totalIn * 100) / 100;
|
|
5862
|
+
bus.emit({
|
|
5863
|
+
kind: "cache_summary",
|
|
5864
|
+
runId,
|
|
5865
|
+
turn: draft.iterations,
|
|
5866
|
+
cacheHitRatio,
|
|
5867
|
+
cacheReadTokens: cacheRead,
|
|
5868
|
+
cacheCreationTokens: cacheCreate
|
|
5869
|
+
});
|
|
5389
5870
|
return {
|
|
5390
5871
|
planText: draft.text,
|
|
5391
5872
|
budgetExhausted: draft.budgetExhausted ?? budgetExhausted,
|
|
@@ -5401,6 +5882,26 @@ async function runPlanner(input5) {
|
|
|
5401
5882
|
const bus = input5.events ?? new PlannerEventBus();
|
|
5402
5883
|
const runId = input5.runId ?? newRunId();
|
|
5403
5884
|
const cacheEnabled = input5.cacheEnabled ?? true;
|
|
5885
|
+
const shouldPersistEvents = input5.persistEvents !== false && input5.paths !== void 0;
|
|
5886
|
+
if (input5.persistEvents === true && !input5.paths) {
|
|
5887
|
+
throw new Error("runPlanner: `paths` is required when `persistEvents: true`");
|
|
5888
|
+
}
|
|
5889
|
+
let persistFinalized = false;
|
|
5890
|
+
if (shouldPersistEvents && input5.paths) {
|
|
5891
|
+
const persistPaths = input5.paths;
|
|
5892
|
+
const store = createRunEventsStore(persistPaths, runId);
|
|
5893
|
+
const unsubPersist = bus.subscribe((e) => {
|
|
5894
|
+
void store.append(e);
|
|
5895
|
+
});
|
|
5896
|
+
bus.finalizeEventPersistence = async () => {
|
|
5897
|
+
if (persistFinalized) return;
|
|
5898
|
+
persistFinalized = true;
|
|
5899
|
+
unsubPersist();
|
|
5900
|
+
await store.close();
|
|
5901
|
+
delete bus.finalizeEventPersistence;
|
|
5902
|
+
await rotateRunEvents(persistPaths);
|
|
5903
|
+
};
|
|
5904
|
+
}
|
|
5404
5905
|
bus.emit({
|
|
5405
5906
|
kind: "started",
|
|
5406
5907
|
runId,
|
|
@@ -5409,6 +5910,27 @@ async function runPlanner(input5) {
|
|
|
5409
5910
|
cacheEnabled,
|
|
5410
5911
|
plannerRuntime: input5.runtime.kind
|
|
5411
5912
|
});
|
|
5913
|
+
bus.emit({
|
|
5914
|
+
kind: "runtime_info",
|
|
5915
|
+
runId,
|
|
5916
|
+
provider: input5.provider,
|
|
5917
|
+
model: input5.modelId,
|
|
5918
|
+
runtimeKind: input5.runtime.kind,
|
|
5919
|
+
cacheEnabled,
|
|
5920
|
+
scoutEnabled: input5.stages?.scout?.enabled !== false,
|
|
5921
|
+
validationEnabled: input5.validation?.enabled !== false,
|
|
5922
|
+
budgetCaps: input5.budget.caps(),
|
|
5923
|
+
providerOptions: input5.provider === "anthropic" && input5.anthropicProviderSpecific ? {
|
|
5924
|
+
anthropic: {
|
|
5925
|
+
thinking: input5.anthropicProviderSpecific.draft?.thinking ?? "adaptive",
|
|
5926
|
+
effort: input5.anthropicProviderSpecific.draft?.effort,
|
|
5927
|
+
effortByPhase: {
|
|
5928
|
+
scout: input5.anthropicProviderSpecific.scout?.effort,
|
|
5929
|
+
draft: input5.anthropicProviderSpecific.draft?.effort
|
|
5930
|
+
}
|
|
5931
|
+
}
|
|
5932
|
+
} : void 0
|
|
5933
|
+
});
|
|
5412
5934
|
const merged = { ...input5, events: bus, runId };
|
|
5413
5935
|
const systemPrompt = input5.systemPrompt;
|
|
5414
5936
|
let scoutSummary;
|
|
@@ -5665,14 +6187,165 @@ var BUILTIN_TOOL_DENY = [
|
|
|
5665
6187
|
"WebFetch",
|
|
5666
6188
|
"WebSearch"
|
|
5667
6189
|
];
|
|
5668
|
-
function
|
|
5669
|
-
|
|
6190
|
+
function initialThinkingState() {
|
|
6191
|
+
return {
|
|
6192
|
+
blockIndexCounter: 0,
|
|
6193
|
+
inThinkingBlock: false,
|
|
6194
|
+
thinkingBlockIndex: -1,
|
|
6195
|
+
thinkingBlockStartedAt: 0,
|
|
6196
|
+
thinkingChars: 0
|
|
6197
|
+
};
|
|
6198
|
+
}
|
|
6199
|
+
function decodeStreamEvent(ev) {
|
|
6200
|
+
if (!ev || typeof ev !== "object") return { kind: "other" };
|
|
5670
6201
|
const o = ev;
|
|
5671
|
-
|
|
6202
|
+
const t = o.type;
|
|
6203
|
+
if (t === "message_start") {
|
|
6204
|
+
const msg = o.message;
|
|
6205
|
+
const u = msg?.usage ?? o.usage;
|
|
6206
|
+
if (!u || typeof u !== "object") return { kind: "message_start" };
|
|
6207
|
+
return {
|
|
6208
|
+
kind: "message_start",
|
|
6209
|
+
usage: {
|
|
6210
|
+
input_tokens: num(u.input_tokens),
|
|
6211
|
+
output_tokens: num(u.output_tokens),
|
|
6212
|
+
cache_creation_input_tokens: num(u.cache_creation_input_tokens),
|
|
6213
|
+
cache_read_input_tokens: num(u.cache_read_input_tokens)
|
|
6214
|
+
}
|
|
6215
|
+
};
|
|
6216
|
+
}
|
|
6217
|
+
if (t === "message_delta") {
|
|
6218
|
+
const u = o.usage;
|
|
6219
|
+
if (!u || typeof u !== "object") return { kind: "message_delta" };
|
|
6220
|
+
return {
|
|
6221
|
+
kind: "message_delta",
|
|
6222
|
+
usage: {
|
|
6223
|
+
output_tokens: num(u.output_tokens),
|
|
6224
|
+
cache_creation_input_tokens: num(u.cache_creation_input_tokens),
|
|
6225
|
+
cache_read_input_tokens: num(u.cache_read_input_tokens)
|
|
6226
|
+
}
|
|
6227
|
+
};
|
|
6228
|
+
}
|
|
6229
|
+
if (t === "content_block_start") {
|
|
6230
|
+
const idx = typeof o.index === "number" ? o.index : 0;
|
|
6231
|
+
const cb = o.content_block;
|
|
6232
|
+
const bt = cb?.type;
|
|
6233
|
+
if (bt === "thinking" || bt === "text" || bt === "tool_use")
|
|
6234
|
+
return { kind: "content_block_start", blockType: bt, index: idx };
|
|
6235
|
+
return { kind: "other" };
|
|
6236
|
+
}
|
|
6237
|
+
if (t === "content_block_stop") {
|
|
6238
|
+
return { kind: "content_block_stop", index: typeof o.index === "number" ? o.index : 0 };
|
|
6239
|
+
}
|
|
6240
|
+
if (t === "content_block_delta") {
|
|
5672
6241
|
const d = o.delta;
|
|
5673
|
-
if (d
|
|
6242
|
+
if (!d) return { kind: "other" };
|
|
6243
|
+
const dt = d.type;
|
|
6244
|
+
if (dt === "text_delta" && typeof d.text === "string") {
|
|
6245
|
+
return { kind: "text_delta", text: d.text };
|
|
6246
|
+
}
|
|
6247
|
+
if (dt === "thinking_delta") {
|
|
6248
|
+
const th = typeof d.thinking === "string" ? d.thinking : typeof d.text === "string" ? d.text : "";
|
|
6249
|
+
if (th) return { kind: "thinking_delta", text: th };
|
|
6250
|
+
}
|
|
6251
|
+
}
|
|
6252
|
+
return { kind: "other" };
|
|
6253
|
+
}
|
|
6254
|
+
function num(v) {
|
|
6255
|
+
return typeof v === "number" && !Number.isNaN(v) ? v : void 0;
|
|
6256
|
+
}
|
|
6257
|
+
function handleDecodedStreamEvent(decoded, input5) {
|
|
6258
|
+
const { bus, runId, onAssistantText, think } = input5;
|
|
6259
|
+
const turnUse = input5.assistantTurns + 1;
|
|
6260
|
+
if (decoded.kind === "message_start" && decoded.usage) {
|
|
6261
|
+
const u = decoded.usage;
|
|
6262
|
+
const partial = {
|
|
6263
|
+
inputTokens: u.input_tokens ?? 0,
|
|
6264
|
+
outputTokens: u.output_tokens ?? 0,
|
|
6265
|
+
cacheCreationTokens: u.cache_creation_input_tokens ?? 0,
|
|
6266
|
+
cacheReadTokens: u.cache_read_input_tokens ?? 0
|
|
6267
|
+
};
|
|
6268
|
+
bus.emit({ kind: "usage", runId, turn: turnUse, usage: partial });
|
|
6269
|
+
return;
|
|
6270
|
+
}
|
|
6271
|
+
if (decoded.kind === "message_delta" && decoded.usage) {
|
|
6272
|
+
const out = decoded.usage.output_tokens ?? 0;
|
|
6273
|
+
if (out > 0) {
|
|
6274
|
+
bus.emit({
|
|
6275
|
+
kind: "usage",
|
|
6276
|
+
runId,
|
|
6277
|
+
turn: turnUse,
|
|
6278
|
+
usage: { inputTokens: 0, outputTokens: out }
|
|
6279
|
+
});
|
|
6280
|
+
}
|
|
6281
|
+
return;
|
|
6282
|
+
}
|
|
6283
|
+
if (decoded.kind === "text_delta") {
|
|
6284
|
+
input5.textSink?.append(decoded.text);
|
|
6285
|
+
onAssistantText?.(decoded.text);
|
|
6286
|
+
bus.emit({
|
|
6287
|
+
kind: "assistant_text",
|
|
6288
|
+
runId,
|
|
6289
|
+
turn: input5.assistantTurns,
|
|
6290
|
+
delta: decoded.text
|
|
6291
|
+
});
|
|
6292
|
+
return;
|
|
6293
|
+
}
|
|
6294
|
+
if (decoded.kind === "content_block_start") {
|
|
6295
|
+
if (decoded.blockType === "thinking") {
|
|
6296
|
+
think.inThinkingBlock = true;
|
|
6297
|
+
think.thinkingBlockIndex = decoded.index;
|
|
6298
|
+
think.thinkingChars = 0;
|
|
6299
|
+
think.thinkingBlockStartedAt = Date.now();
|
|
6300
|
+
think.blockIndexCounter = Math.max(think.blockIndexCounter, decoded.index + 1);
|
|
6301
|
+
bus.emit({
|
|
6302
|
+
kind: "thinking_block_started",
|
|
6303
|
+
runId,
|
|
6304
|
+
turn: input5.assistantTurns,
|
|
6305
|
+
blockIndex: think.thinkingBlockIndex
|
|
6306
|
+
});
|
|
6307
|
+
} else {
|
|
6308
|
+
think.inThinkingBlock = false;
|
|
6309
|
+
}
|
|
6310
|
+
return;
|
|
6311
|
+
}
|
|
6312
|
+
if (decoded.kind === "thinking_delta") {
|
|
6313
|
+
if (!think.inThinkingBlock) {
|
|
6314
|
+
think.thinkingBlockIndex = think.blockIndexCounter++;
|
|
6315
|
+
think.thinkingChars = 0;
|
|
6316
|
+
think.thinkingBlockStartedAt = Date.now();
|
|
6317
|
+
think.inThinkingBlock = true;
|
|
6318
|
+
bus.emit({
|
|
6319
|
+
kind: "thinking_block_started",
|
|
6320
|
+
runId,
|
|
6321
|
+
turn: input5.assistantTurns,
|
|
6322
|
+
blockIndex: think.thinkingBlockIndex
|
|
6323
|
+
});
|
|
6324
|
+
}
|
|
6325
|
+
think.thinkingChars += decoded.text.length;
|
|
6326
|
+
bus.emit({
|
|
6327
|
+
kind: "thinking_delta",
|
|
6328
|
+
runId,
|
|
6329
|
+
turn: input5.assistantTurns,
|
|
6330
|
+
blockIndex: think.thinkingBlockIndex,
|
|
6331
|
+
delta: decoded.text
|
|
6332
|
+
});
|
|
6333
|
+
return;
|
|
6334
|
+
}
|
|
6335
|
+
if (decoded.kind === "content_block_stop") {
|
|
6336
|
+
if (think.inThinkingBlock) {
|
|
6337
|
+
bus.emit({
|
|
6338
|
+
kind: "thinking_block_stopped",
|
|
6339
|
+
runId,
|
|
6340
|
+
turn: input5.assistantTurns,
|
|
6341
|
+
blockIndex: think.thinkingBlockIndex,
|
|
6342
|
+
durationMs: Date.now() - think.thinkingBlockStartedAt,
|
|
6343
|
+
chars: think.thinkingChars
|
|
6344
|
+
});
|
|
6345
|
+
think.inThinkingBlock = false;
|
|
6346
|
+
think.thinkingBlockIndex = -1;
|
|
6347
|
+
}
|
|
5674
6348
|
}
|
|
5675
|
-
return "";
|
|
5676
6349
|
}
|
|
5677
6350
|
var AgentSdkRuntime = class {
|
|
5678
6351
|
constructor(modelId, apiKey) {
|
|
@@ -5727,6 +6400,7 @@ var AgentSdkRuntime = class {
|
|
|
5727
6400
|
let finishedNormally = false;
|
|
5728
6401
|
let incompleteKind;
|
|
5729
6402
|
let userCancelled = false;
|
|
6403
|
+
const think = initialThinkingState();
|
|
5730
6404
|
try {
|
|
5731
6405
|
const q = query({
|
|
5732
6406
|
prompt: input5.userMessage,
|
|
@@ -5736,15 +6410,21 @@ var AgentSdkRuntime = class {
|
|
|
5736
6410
|
const m = message;
|
|
5737
6411
|
switch (m.type) {
|
|
5738
6412
|
case "stream_event": {
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
6413
|
+
handleDecodedStreamEvent(decodeStreamEvent(m.event), {
|
|
6414
|
+
bus: input5.bus,
|
|
6415
|
+
runId: input5.runId,
|
|
6416
|
+
assistantTurns,
|
|
6417
|
+
onAssistantText: input5.onAssistantText,
|
|
6418
|
+
think,
|
|
6419
|
+
textSink: { append: (s) => text += s }
|
|
6420
|
+
});
|
|
5745
6421
|
break;
|
|
5746
6422
|
}
|
|
5747
6423
|
case "assistant": {
|
|
6424
|
+
think.blockIndexCounter = 0;
|
|
6425
|
+
think.inThinkingBlock = false;
|
|
6426
|
+
think.thinkingBlockIndex = -1;
|
|
6427
|
+
think.thinkingChars = 0;
|
|
5748
6428
|
assistantTurns += 1;
|
|
5749
6429
|
input5.bus.emit({
|
|
5750
6430
|
kind: "turn_complete",
|
|
@@ -5770,6 +6450,8 @@ var AgentSdkRuntime = class {
|
|
|
5770
6450
|
}
|
|
5771
6451
|
break;
|
|
5772
6452
|
}
|
|
6453
|
+
case "system":
|
|
6454
|
+
break;
|
|
5773
6455
|
default:
|
|
5774
6456
|
break;
|
|
5775
6457
|
}
|
|
@@ -5824,6 +6506,7 @@ var AgentSdkRuntime = class {
|
|
|
5824
6506
|
mcpServers: { "squad-kit-scout": mcpServer },
|
|
5825
6507
|
systemPrompt: input5.systemPrompt + "\n\nIMPORTANT: respond by calling the `respond_with_scout_result` tool with the JSON object \u2014 do not write the JSON as text.",
|
|
5826
6508
|
maxTurns: 2,
|
|
6509
|
+
includePartialMessages: true,
|
|
5827
6510
|
persistSession: false,
|
|
5828
6511
|
settingSources: [],
|
|
5829
6512
|
env: { ...process.env, ANTHROPIC_API_KEY: this.apiKey },
|
|
@@ -5831,6 +6514,8 @@ var AgentSdkRuntime = class {
|
|
|
5831
6514
|
effort
|
|
5832
6515
|
};
|
|
5833
6516
|
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
6517
|
+
let assistantTurns = 0;
|
|
6518
|
+
const think = initialThinkingState();
|
|
5834
6519
|
const ac = new AbortController();
|
|
5835
6520
|
const onAbort = () => ac.abort();
|
|
5836
6521
|
if (input5.abort) {
|
|
@@ -5844,10 +6529,31 @@ var AgentSdkRuntime = class {
|
|
|
5844
6529
|
});
|
|
5845
6530
|
for await (const message of q) {
|
|
5846
6531
|
const m = message;
|
|
5847
|
-
|
|
5848
|
-
|
|
5849
|
-
m.
|
|
5850
|
-
|
|
6532
|
+
switch (m.type) {
|
|
6533
|
+
case "stream_event":
|
|
6534
|
+
handleDecodedStreamEvent(decodeStreamEvent(m.event), {
|
|
6535
|
+
bus: input5.bus,
|
|
6536
|
+
runId: input5.runId,
|
|
6537
|
+
assistantTurns,
|
|
6538
|
+
think
|
|
6539
|
+
});
|
|
6540
|
+
break;
|
|
6541
|
+
case "assistant":
|
|
6542
|
+
think.blockIndexCounter = 0;
|
|
6543
|
+
think.inThinkingBlock = false;
|
|
6544
|
+
think.thinkingBlockIndex = -1;
|
|
6545
|
+
think.thinkingChars = 0;
|
|
6546
|
+
assistantTurns += 1;
|
|
6547
|
+
break;
|
|
6548
|
+
case "result":
|
|
6549
|
+
usage = usageFromAgentSdkResult(
|
|
6550
|
+
m.usage
|
|
6551
|
+
);
|
|
6552
|
+
break;
|
|
6553
|
+
case "system":
|
|
6554
|
+
break;
|
|
6555
|
+
default:
|
|
6556
|
+
break;
|
|
5851
6557
|
}
|
|
5852
6558
|
}
|
|
5853
6559
|
} finally {
|
|
@@ -5927,6 +6633,13 @@ var Budget = class {
|
|
|
5927
6633
|
timedOut() {
|
|
5928
6634
|
return (Date.now() - this.startedAt) / 1e3 > this.maxWallSeconds;
|
|
5929
6635
|
}
|
|
6636
|
+
caps() {
|
|
6637
|
+
return {
|
|
6638
|
+
maxFileReads: this.maxFileReadsAllowed,
|
|
6639
|
+
maxContextBytes: this.maxContextBytesAllowed,
|
|
6640
|
+
maxDurationSeconds: this.maxWallSeconds
|
|
6641
|
+
};
|
|
6642
|
+
}
|
|
5930
6643
|
snapshot() {
|
|
5931
6644
|
return {
|
|
5932
6645
|
reads: this.reads,
|
|
@@ -5938,8 +6651,8 @@ var Budget = class {
|
|
|
5938
6651
|
};
|
|
5939
6652
|
|
|
5940
6653
|
// src/planner/writer.ts
|
|
5941
|
-
import
|
|
5942
|
-
import
|
|
6654
|
+
import fs29 from "fs";
|
|
6655
|
+
import path21 from "path";
|
|
5943
6656
|
function planFileSlugParts(config2, story) {
|
|
5944
6657
|
const baseSlug = slugify(story.id);
|
|
5945
6658
|
const trackerId = config2.naming.includeTrackerId && config2.tracker.type !== "none" ? trackerIdForFilename(config2.tracker.type, story.id) : "";
|
|
@@ -5948,8 +6661,8 @@ function planFileSlugParts(config2, story) {
|
|
|
5948
6661
|
}
|
|
5949
6662
|
function writePlanFile(input5) {
|
|
5950
6663
|
const { paths, config: config2, story } = input5;
|
|
5951
|
-
const featurePlanDir =
|
|
5952
|
-
|
|
6664
|
+
const featurePlanDir = path21.join(paths.plansDir, story.feature);
|
|
6665
|
+
fs29.mkdirSync(featurePlanDir, { recursive: true });
|
|
5953
6666
|
if (input5.partial) {
|
|
5954
6667
|
return writePartialPlanFile(input5, featurePlanDir, paths, config2, story);
|
|
5955
6668
|
}
|
|
@@ -5964,14 +6677,14 @@ function writePlanFile(input5) {
|
|
|
5964
6677
|
} else {
|
|
5965
6678
|
nn = scanPlans(paths.plansDir).nextGlobal;
|
|
5966
6679
|
const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
|
|
5967
|
-
targetPath =
|
|
6680
|
+
targetPath = path21.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.md`);
|
|
5968
6681
|
}
|
|
5969
6682
|
const body = `${input5.metadataHeader.trimEnd()}
|
|
5970
6683
|
|
|
5971
6684
|
${input5.planBodyMarkdown.trimStart()}
|
|
5972
6685
|
`;
|
|
5973
|
-
|
|
5974
|
-
upsertOverviewRow(featurePlanDir, story.feature, nn,
|
|
6686
|
+
fs29.writeFileSync(targetPath, body, "utf8");
|
|
6687
|
+
upsertOverviewRow(featurePlanDir, story.feature, nn, path21.basename(targetPath), story, false);
|
|
5975
6688
|
return { planFile: targetPath, sequenceNumber: nn, overwrote };
|
|
5976
6689
|
}
|
|
5977
6690
|
function writePartialPlanFile(input5, featurePlanDir, paths, config2, story) {
|
|
@@ -5986,42 +6699,42 @@ function writePartialPlanFile(input5, featurePlanDir, paths, config2, story) {
|
|
|
5986
6699
|
} else {
|
|
5987
6700
|
nn = scanPlans(paths.plansDir).nextGlobal;
|
|
5988
6701
|
const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
|
|
5989
|
-
targetPath =
|
|
6702
|
+
targetPath = path21.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.partial.md`);
|
|
5990
6703
|
}
|
|
5991
6704
|
const body = `${input5.metadataHeader.trimEnd()}
|
|
5992
6705
|
|
|
5993
6706
|
${input5.planBodyMarkdown.trimStart()}
|
|
5994
6707
|
`;
|
|
5995
|
-
|
|
5996
|
-
upsertOverviewRow(featurePlanDir, story.feature, nn,
|
|
6708
|
+
fs29.writeFileSync(targetPath, body, "utf8");
|
|
6709
|
+
upsertOverviewRow(featurePlanDir, story.feature, nn, path21.basename(targetPath), story, true);
|
|
5997
6710
|
return { planFile: targetPath, sequenceNumber: nn, overwrote };
|
|
5998
6711
|
}
|
|
5999
6712
|
function findExistingPlan(featureDir, storyId) {
|
|
6000
|
-
if (!
|
|
6713
|
+
if (!fs29.existsSync(featureDir)) return void 0;
|
|
6001
6714
|
const pattern = /^(\d{2,})-story-.+\.md$/;
|
|
6002
|
-
for (const entry of
|
|
6715
|
+
for (const entry of fs29.readdirSync(featureDir)) {
|
|
6003
6716
|
if (entry.endsWith(".partial.md")) continue;
|
|
6004
6717
|
const m = entry.match(pattern);
|
|
6005
6718
|
if (!m) continue;
|
|
6006
|
-
if (entry.includes(storyId)) return { absPath:
|
|
6719
|
+
if (entry.includes(storyId)) return { absPath: path21.join(featureDir, entry), nn: parseInt(m[1], 10) };
|
|
6007
6720
|
}
|
|
6008
6721
|
return void 0;
|
|
6009
6722
|
}
|
|
6010
6723
|
function findExistingPartialPlan(featureDir, storyId) {
|
|
6011
|
-
if (!
|
|
6724
|
+
if (!fs29.existsSync(featureDir)) return void 0;
|
|
6012
6725
|
const pattern = /^(\d{2,})-story-.+\.partial\.md$/;
|
|
6013
|
-
for (const entry of
|
|
6726
|
+
for (const entry of fs29.readdirSync(featureDir)) {
|
|
6014
6727
|
const m = entry.match(pattern);
|
|
6015
6728
|
if (!m) continue;
|
|
6016
|
-
if (entry.includes(storyId)) return { absPath:
|
|
6729
|
+
if (entry.includes(storyId)) return { absPath: path21.join(featureDir, entry), nn: parseInt(m[1], 10) };
|
|
6017
6730
|
}
|
|
6018
6731
|
return void 0;
|
|
6019
6732
|
}
|
|
6020
6733
|
function upsertOverviewRow(featureDir, feature, nn, filename, story, isPartial) {
|
|
6021
|
-
const overviewPath =
|
|
6734
|
+
const overviewPath = path21.join(featureDir, "00-overview.md");
|
|
6022
6735
|
const titleCell = isPartial ? `${story.titleHint ?? story.id} (partial)` : story.titleHint ?? story.id;
|
|
6023
6736
|
const row = `| ${formatSequence(nn)} | \`${filename}\` | ${titleCell} | ${story.id} | \u2014 |`;
|
|
6024
|
-
if (!
|
|
6737
|
+
if (!fs29.existsSync(overviewPath)) {
|
|
6025
6738
|
const content = `# ${feature} \u2014 plan overview
|
|
6026
6739
|
|
|
6027
6740
|
Entry point for the **${feature}** feature. Stories execute in order by their \`NN\` prefix.
|
|
@@ -6032,10 +6745,10 @@ Entry point for the **${feature}** feature. Stories execute in order by their \`
|
|
|
6032
6745
|
|----|------|-------|------------|------------|
|
|
6033
6746
|
${row}
|
|
6034
6747
|
`;
|
|
6035
|
-
|
|
6748
|
+
fs29.writeFileSync(overviewPath, content, "utf8");
|
|
6036
6749
|
return;
|
|
6037
6750
|
}
|
|
6038
|
-
const current =
|
|
6751
|
+
const current = fs29.readFileSync(overviewPath, "utf8");
|
|
6039
6752
|
const lines = current.split("\n");
|
|
6040
6753
|
const existingIdx = lines.findIndex((l) => l.includes(`\`${filename}\``) || l.includes(`| ${formatSequence(nn)} |`));
|
|
6041
6754
|
if (existingIdx >= 0) {
|
|
@@ -6044,7 +6757,7 @@ ${row}
|
|
|
6044
6757
|
const tableEnd = findTableEnd(lines);
|
|
6045
6758
|
lines.splice(tableEnd, 0, row);
|
|
6046
6759
|
}
|
|
6047
|
-
|
|
6760
|
+
fs29.writeFileSync(overviewPath, lines.join("\n"), "utf8");
|
|
6048
6761
|
}
|
|
6049
6762
|
function findTableEnd(lines) {
|
|
6050
6763
|
let inTable = false;
|
|
@@ -6097,16 +6810,16 @@ function buildCopyPlanPromptMarkdown(config2, intakeContent) {
|
|
|
6097
6810
|
}
|
|
6098
6811
|
|
|
6099
6812
|
// src/core/last-run.ts
|
|
6100
|
-
import
|
|
6101
|
-
import
|
|
6813
|
+
import fs30 from "fs/promises";
|
|
6814
|
+
import path22 from "path";
|
|
6102
6815
|
async function writeLastRun(paths, rec) {
|
|
6103
|
-
const file =
|
|
6104
|
-
await
|
|
6816
|
+
const file = path22.join(paths.squadDir, ".last-run.json");
|
|
6817
|
+
await fs30.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
|
|
6105
6818
|
}
|
|
6106
6819
|
async function readLastRun(paths) {
|
|
6107
|
-
const file =
|
|
6820
|
+
const file = path22.join(paths.squadDir, ".last-run.json");
|
|
6108
6821
|
try {
|
|
6109
|
-
const raw = await
|
|
6822
|
+
const raw = await fs30.readFile(file, "utf8");
|
|
6110
6823
|
const parsed = JSON.parse(raw);
|
|
6111
6824
|
if (parsed.version !== 1) return void 0;
|
|
6112
6825
|
return parsed;
|
|
@@ -6139,8 +6852,8 @@ function formatPlannerCacheLine(opts) {
|
|
|
6139
6852
|
}
|
|
6140
6853
|
|
|
6141
6854
|
// src/core/plans-index.ts
|
|
6142
|
-
import
|
|
6143
|
-
import
|
|
6855
|
+
import fs31 from "fs";
|
|
6856
|
+
import path23 from "path";
|
|
6144
6857
|
var MAX_ENTRIES = 30;
|
|
6145
6858
|
function firstMarkdownH1(src) {
|
|
6146
6859
|
const lines = src.split(/\r?\n/);
|
|
@@ -6155,12 +6868,12 @@ function collectPlanMarkdownFiles(plansDir) {
|
|
|
6155
6868
|
const walk2 = (dir) => {
|
|
6156
6869
|
let ents;
|
|
6157
6870
|
try {
|
|
6158
|
-
ents =
|
|
6871
|
+
ents = fs31.readdirSync(dir, { withFileTypes: true });
|
|
6159
6872
|
} catch {
|
|
6160
6873
|
return;
|
|
6161
6874
|
}
|
|
6162
6875
|
for (const ent of ents) {
|
|
6163
|
-
const abs =
|
|
6876
|
+
const abs = path23.join(dir, ent.name);
|
|
6164
6877
|
if (ent.isDirectory()) {
|
|
6165
6878
|
walk2(abs);
|
|
6166
6879
|
} else if (ent.isFile() && ent.name.endsWith(".md")) {
|
|
@@ -6169,7 +6882,7 @@ function collectPlanMarkdownFiles(plansDir) {
|
|
|
6169
6882
|
}
|
|
6170
6883
|
}
|
|
6171
6884
|
};
|
|
6172
|
-
if (
|
|
6885
|
+
if (fs31.existsSync(plansDir)) walk2(plansDir);
|
|
6173
6886
|
out.sort((a, b) => a.localeCompare(b));
|
|
6174
6887
|
return out.slice(0, MAX_ENTRIES);
|
|
6175
6888
|
}
|
|
@@ -6178,19 +6891,265 @@ function buildPlansIndex(paths) {
|
|
|
6178
6891
|
if (files.length === 0) return "(no prior plans yet)";
|
|
6179
6892
|
const bullets = [];
|
|
6180
6893
|
for (const abs of files) {
|
|
6181
|
-
const rel =
|
|
6894
|
+
const rel = path23.relative(paths.root, abs).split(path23.sep).join("/");
|
|
6182
6895
|
let title = "";
|
|
6183
6896
|
try {
|
|
6184
|
-
const raw =
|
|
6185
|
-
title = firstMarkdownH1(raw) ??
|
|
6897
|
+
const raw = fs31.readFileSync(abs, "utf8");
|
|
6898
|
+
title = firstMarkdownH1(raw) ?? path23.basename(abs);
|
|
6186
6899
|
} catch {
|
|
6187
|
-
title =
|
|
6900
|
+
title = path23.basename(abs);
|
|
6188
6901
|
}
|
|
6189
6902
|
bullets.push(`- ${rel} \u2014 "${title}"`);
|
|
6190
6903
|
}
|
|
6191
6904
|
return bullets.join("\n");
|
|
6192
6905
|
}
|
|
6193
6906
|
|
|
6907
|
+
// src/commands/new-plan-api-events.ts
|
|
6908
|
+
function toolLabel(name, inp) {
|
|
6909
|
+
if (name === "read_file") {
|
|
6910
|
+
return `read ${String(inp.path ?? "<unknown>")}`;
|
|
6911
|
+
}
|
|
6912
|
+
if (name === "grep") {
|
|
6913
|
+
return `grep ${String(inp.pattern ?? "?").slice(0, 48)}`;
|
|
6914
|
+
}
|
|
6915
|
+
if (name === "list_dir") {
|
|
6916
|
+
return `list_dir ${String(inp.path ?? "?")}`;
|
|
6917
|
+
}
|
|
6918
|
+
return name;
|
|
6919
|
+
}
|
|
6920
|
+
function ensureStagesIntro(ctx) {
|
|
6921
|
+
if (ctx.stagesIntroPrinted) return;
|
|
6922
|
+
ctx.stagesIntroPrinted = true;
|
|
6923
|
+
divider("stages");
|
|
6924
|
+
}
|
|
6925
|
+
function budgetSnap(ctx) {
|
|
6926
|
+
const snap = ctx.budget.snapshot();
|
|
6927
|
+
const caps = ctx.budgetCaps ?? ctx.budget.caps();
|
|
6928
|
+
return {
|
|
6929
|
+
reads: snap.reads,
|
|
6930
|
+
readsCap: caps.maxFileReads,
|
|
6931
|
+
bytes: snap.bytes,
|
|
6932
|
+
bytesCap: caps.maxContextBytes,
|
|
6933
|
+
elapsedMs: Date.now() - ctx.startedAt,
|
|
6934
|
+
durationMsCap: caps.maxDurationSeconds * 1e3
|
|
6935
|
+
};
|
|
6936
|
+
}
|
|
6937
|
+
function printBudgetMeterLine(ctx) {
|
|
6938
|
+
line(` ${dim(formatBudgetMeter(budgetSnap(ctx)))}`);
|
|
6939
|
+
}
|
|
6940
|
+
function resumeSessionSpinner(ctx) {
|
|
6941
|
+
const st = ctx.activePlannerStage;
|
|
6942
|
+
if (st === "scout") ctx.sessionSpinner.current = spinner("scouting\u2026");
|
|
6943
|
+
else if (st === "draft") ctx.sessionSpinner.current = spinner("drafting\u2026");
|
|
6944
|
+
else if (st === "validation") ctx.sessionSpinner.current = spinner("validating\u2026");
|
|
6945
|
+
else ctx.sessionSpinner.current = spinner("next tool\u2026");
|
|
6946
|
+
}
|
|
6947
|
+
function clearThinkingTick(ctx) {
|
|
6948
|
+
if (ctx.thinkingTick.id) {
|
|
6949
|
+
clearInterval(ctx.thinkingTick.id);
|
|
6950
|
+
ctx.thinkingTick.id = null;
|
|
6951
|
+
}
|
|
6952
|
+
}
|
|
6953
|
+
function applyUsagePrefix(ctx) {
|
|
6954
|
+
if (!ctx.interactive || !ctx.sessionSpinner.current) return;
|
|
6955
|
+
const snap = ctx.budget.snapshot().usage;
|
|
6956
|
+
const useBudget = snap.inputTokens > 0 || snap.outputTokens > 0;
|
|
6957
|
+
const inTok = useBudget ? snap.inputTokens : ctx.usageAcc.inputTokens;
|
|
6958
|
+
const outTok = useBudget ? snap.outputTokens : ctx.usageAcc.outputTokens;
|
|
6959
|
+
const meter = formatBudgetMeter(budgetSnap(ctx));
|
|
6960
|
+
ctx.sessionSpinner.current.setPrefix(`${inTok} in \xB7 ${outTok} out \xB7 ${meter} `);
|
|
6961
|
+
}
|
|
6962
|
+
function dispatchNewPlanApiPlannerEvent(ctx, e) {
|
|
6963
|
+
switch (e.kind) {
|
|
6964
|
+
case "runtime_info": {
|
|
6965
|
+
ctx.budgetCaps = e.budgetCaps;
|
|
6966
|
+
if (!e.scoutEnabled) {
|
|
6967
|
+
ctx.stageLine.scout = "skipped";
|
|
6968
|
+
}
|
|
6969
|
+
if (!e.validationEnabled) {
|
|
6970
|
+
ctx.stageLine.validation = "skipped";
|
|
6971
|
+
}
|
|
6972
|
+
divider("runtime info");
|
|
6973
|
+
kv("provider", e.provider);
|
|
6974
|
+
kv("model", e.model);
|
|
6975
|
+
kv(
|
|
6976
|
+
"runtime",
|
|
6977
|
+
`${e.runtimeKind}${e.provider === "anthropic" && ctx.anthropicRuntimeChoice ? ` (${ctx.anthropicRuntimeChoice})` : ""}`
|
|
6978
|
+
);
|
|
6979
|
+
kv("cache", e.cacheEnabled ? "enabled" : "disabled");
|
|
6980
|
+
kv("scout", e.scoutEnabled ? "enabled" : "disabled");
|
|
6981
|
+
kv("validation", e.validationEnabled ? "enabled" : "disabled");
|
|
6982
|
+
const caps = e.budgetCaps;
|
|
6983
|
+
kv(
|
|
6984
|
+
"budget caps",
|
|
6985
|
+
`${caps.maxFileReads} reads \xB7 ${(caps.maxContextBytes / 1024).toFixed(0)} KB \xB7 ${caps.maxDurationSeconds}s`
|
|
6986
|
+
);
|
|
6987
|
+
const anth = e.providerOptions?.anthropic;
|
|
6988
|
+
if (e.provider === "anthropic" && anth) {
|
|
6989
|
+
if (anth.effortByPhase?.scout || anth.effortByPhase?.draft) {
|
|
6990
|
+
const s = anth.effortByPhase.scout ? `scout ${anth.effortByPhase.scout}` : "";
|
|
6991
|
+
const d = anth.effortByPhase.draft ? `draft ${anth.effortByPhase.draft}` : "";
|
|
6992
|
+
kv("effort", [s, d].filter(Boolean).join(" \xB7 ") || (anth.effort ?? "\u2014"));
|
|
6993
|
+
} else if (anth.effort) {
|
|
6994
|
+
kv("effort", anth.effort);
|
|
6995
|
+
}
|
|
6996
|
+
if (anth.thinking && anth.thinking !== "off") {
|
|
6997
|
+
kv("thinking", anth.thinking);
|
|
6998
|
+
}
|
|
6999
|
+
}
|
|
7000
|
+
ensureStagesIntro(ctx);
|
|
7001
|
+
printStagePipeline(ctx.stageLine);
|
|
7002
|
+
break;
|
|
7003
|
+
}
|
|
7004
|
+
case "stage_started": {
|
|
7005
|
+
ctx.activePlannerStage = e.stage;
|
|
7006
|
+
ensureStagesIntro(ctx);
|
|
7007
|
+
ctx.stageLine[e.stage] = "running";
|
|
7008
|
+
printStagePipeline(ctx.stageLine);
|
|
7009
|
+
printBudgetMeterLine(ctx);
|
|
7010
|
+
ctx.sessionSpinner.current?.stop();
|
|
7011
|
+
if (e.stage === "scout") ctx.sessionSpinner.current = spinner("scouting\u2026");
|
|
7012
|
+
else if (e.stage === "draft") ctx.sessionSpinner.current = spinner("drafting\u2026");
|
|
7013
|
+
else if (e.stage === "validation") ctx.sessionSpinner.current = spinner("validating\u2026");
|
|
7014
|
+
applyUsagePrefix(ctx);
|
|
7015
|
+
break;
|
|
7016
|
+
}
|
|
7017
|
+
case "stage_complete": {
|
|
7018
|
+
const st = e.stage;
|
|
7019
|
+
ctx.stageLine[st] = e.success ? "success" : "failed";
|
|
7020
|
+
if (!e.success && e.errorMessage && st === "scout") {
|
|
7021
|
+
ctx.sessionSpinner.current?.stop();
|
|
7022
|
+
warning(`scout stage failed: ${e.errorMessage}`);
|
|
7023
|
+
}
|
|
7024
|
+
ensureStagesIntro(ctx);
|
|
7025
|
+
printStagePipeline(ctx.stageLine);
|
|
7026
|
+
break;
|
|
7027
|
+
}
|
|
7028
|
+
case "tool_call_started":
|
|
7029
|
+
break;
|
|
7030
|
+
case "tool_call": {
|
|
7031
|
+
const name = e.toolCall.name;
|
|
7032
|
+
const inp = e.toolCall.input;
|
|
7033
|
+
ctx.lastToolUi = {
|
|
7034
|
+
label: toolLabel(name, inp),
|
|
7035
|
+
bytesLoaded: e.bytesLoaded,
|
|
7036
|
+
totalBytes: e.totalBytes
|
|
7037
|
+
};
|
|
7038
|
+
break;
|
|
7039
|
+
}
|
|
7040
|
+
case "tool_call_completed": {
|
|
7041
|
+
const pending = ctx.lastToolUi;
|
|
7042
|
+
ctx.lastToolUi = null;
|
|
7043
|
+
const label = pending?.label ?? toolLabel(e.name, {});
|
|
7044
|
+
const bytesLoaded = pending?.bytesLoaded ?? e.bytesLoaded;
|
|
7045
|
+
const totalBytes = pending?.totalBytes ?? e.totalBytes;
|
|
7046
|
+
const caps = ctx.budgetCaps ?? ctx.budget.caps();
|
|
7047
|
+
const kb = `(${(bytesLoaded / 1024).toFixed(1)} KB \xB7 ${(totalBytes / 1024).toFixed(1)} KB / ${(caps.maxContextBytes / 1024).toFixed(0)} KB) \xB7 tool ${e.durationMs}ms`;
|
|
7048
|
+
if (e.isError) {
|
|
7049
|
+
ctx.sessionSpinner.current?.fail(
|
|
7050
|
+
`${label} ${kb}${e.errorSnippet ? ` \u2014 ${e.errorSnippet}` : ""}`
|
|
7051
|
+
);
|
|
7052
|
+
} else {
|
|
7053
|
+
ctx.sessionSpinner.current?.succeed(`${label} ${kb}`);
|
|
7054
|
+
}
|
|
7055
|
+
ctx.sessionSpinner.current = spinner("next tool\u2026");
|
|
7056
|
+
applyUsagePrefix(ctx);
|
|
7057
|
+
printBudgetMeterLine(ctx);
|
|
7058
|
+
break;
|
|
7059
|
+
}
|
|
7060
|
+
case "thinking_block_started": {
|
|
7061
|
+
clearThinkingTick(ctx);
|
|
7062
|
+
ctx.thinkingState.running = true;
|
|
7063
|
+
ctx.thinkingBlockChars = 0;
|
|
7064
|
+
ctx.thinkingState.blockStartedAt = Date.now();
|
|
7065
|
+
ctx.sessionSpinner.current?.stop();
|
|
7066
|
+
ctx.thinkingSpinner.current = spinner("thinking\u2026");
|
|
7067
|
+
ctx.thinkingTick.id = setInterval(() => {
|
|
7068
|
+
if (!ctx.thinkingSpinner.current) return;
|
|
7069
|
+
ctx.thinkingSpinner.current.update(
|
|
7070
|
+
formatThinkingLine({
|
|
7071
|
+
running: true,
|
|
7072
|
+
durationMs: Date.now() - ctx.thinkingState.blockStartedAt,
|
|
7073
|
+
chars: ctx.thinkingBlockChars
|
|
7074
|
+
})
|
|
7075
|
+
);
|
|
7076
|
+
}, 500);
|
|
7077
|
+
break;
|
|
7078
|
+
}
|
|
7079
|
+
case "thinking_delta": {
|
|
7080
|
+
ctx.thinkingBlockChars += e.delta.length;
|
|
7081
|
+
break;
|
|
7082
|
+
}
|
|
7083
|
+
case "thinking_block_stopped": {
|
|
7084
|
+
clearThinkingTick(ctx);
|
|
7085
|
+
ctx.thinkingState.running = false;
|
|
7086
|
+
ctx.thinkingSpinner.current?.succeed(
|
|
7087
|
+
formatThinkingLine({ running: false, durationMs: e.durationMs, chars: e.chars })
|
|
7088
|
+
);
|
|
7089
|
+
ctx.thinkingSpinner.current = null;
|
|
7090
|
+
ctx.thinkingState.totalChars += e.chars;
|
|
7091
|
+
ctx.thinkingState.totalDurationMs += e.durationMs;
|
|
7092
|
+
resumeSessionSpinner(ctx);
|
|
7093
|
+
applyUsagePrefix(ctx);
|
|
7094
|
+
break;
|
|
7095
|
+
}
|
|
7096
|
+
case "usage": {
|
|
7097
|
+
ctx.usageAcc.inputTokens += e.usage.inputTokens;
|
|
7098
|
+
ctx.usageAcc.outputTokens += e.usage.outputTokens;
|
|
7099
|
+
applyUsagePrefix(ctx);
|
|
7100
|
+
break;
|
|
7101
|
+
}
|
|
7102
|
+
case "validation_issue": {
|
|
7103
|
+
if (!ctx.interactive) break;
|
|
7104
|
+
ctx.streamedValidationIssues.count += 1;
|
|
7105
|
+
const n = ctx.streamedValidationIssues.count;
|
|
7106
|
+
if (n <= 50) {
|
|
7107
|
+
const pathPart = e.path ? ` ${e.path}` : "";
|
|
7108
|
+
const line2 = `[${e.severity}] ${e.issueKind}${pathPart} \u2014 ${e.detail}`;
|
|
7109
|
+
if (e.severity === "error") failure(line2);
|
|
7110
|
+
else warning(line2);
|
|
7111
|
+
} else if (!ctx.validationStreamCappedMsg) {
|
|
7112
|
+
ctx.validationStreamCappedMsg = true;
|
|
7113
|
+
info(` \u2026 +${n - 50} more issues, see summary`);
|
|
7114
|
+
}
|
|
7115
|
+
break;
|
|
7116
|
+
}
|
|
7117
|
+
case "assistant_text": {
|
|
7118
|
+
if (ctx.thinkingState.running) return;
|
|
7119
|
+
ctx.sessionSpinner.current?.succeed("planner thinking complete (this chunk)");
|
|
7120
|
+
ctx.sessionSpinner.current = spinner("thinking\u2026");
|
|
7121
|
+
applyUsagePrefix(ctx);
|
|
7122
|
+
break;
|
|
7123
|
+
}
|
|
7124
|
+
case "rate_limit": {
|
|
7125
|
+
if (e.phase === "retrying") {
|
|
7126
|
+
ctx.sessionSpinner.current?.stop();
|
|
7127
|
+
warning(`${e.provider} rate limit hit \u2014 retrying in ${e.waitSec}s`);
|
|
7128
|
+
ctx.sessionSpinner.current = spinner("waiting for rate limit to reset\u2026");
|
|
7129
|
+
}
|
|
7130
|
+
break;
|
|
7131
|
+
}
|
|
7132
|
+
case "scout_result": {
|
|
7133
|
+
ctx.activePlannerStage = "draft";
|
|
7134
|
+
const preview = e.selected.slice(0, 4).join(", ");
|
|
7135
|
+
const more = e.selected.length > 4 ? "\u2026" : "";
|
|
7136
|
+
ctx.sessionSpinner.current?.succeed(`scout picked ${e.selected.length} files: ${preview}${more}`);
|
|
7137
|
+
ctx.sessionSpinner.current = spinner("drafting\u2026");
|
|
7138
|
+
applyUsagePrefix(ctx);
|
|
7139
|
+
break;
|
|
7140
|
+
}
|
|
7141
|
+
case "cancelled": {
|
|
7142
|
+
clearThinkingTick(ctx);
|
|
7143
|
+
ctx.thinkingState.running = false;
|
|
7144
|
+
ctx.thinkingSpinner.current?.fail("cancelled");
|
|
7145
|
+
ctx.thinkingSpinner.current = null;
|
|
7146
|
+
break;
|
|
7147
|
+
}
|
|
7148
|
+
default:
|
|
7149
|
+
break;
|
|
7150
|
+
}
|
|
7151
|
+
}
|
|
7152
|
+
|
|
6194
7153
|
// src/commands/new-plan.ts
|
|
6195
7154
|
function decideMode(opts, config2) {
|
|
6196
7155
|
if (opts.api && opts.copy) {
|
|
@@ -6222,8 +7181,8 @@ async function runNewPlan(intakePath, opts) {
|
|
|
6222
7181
|
}
|
|
6223
7182
|
}
|
|
6224
7183
|
function resolveFromPath(intakePath, stories, root) {
|
|
6225
|
-
const resolved =
|
|
6226
|
-
if (!
|
|
7184
|
+
const resolved = path24.resolve(intakePath.trim());
|
|
7185
|
+
if (!fs32.existsSync(resolved)) {
|
|
6227
7186
|
throw new Error(
|
|
6228
7187
|
`Intake not found: ${resolved}. Run \`squad list\` to see intakes, or \`squad new-story\` to create one first.`
|
|
6229
7188
|
);
|
|
@@ -6235,7 +7194,7 @@ function resolveFromPath(intakePath, stories, root) {
|
|
|
6235
7194
|
);
|
|
6236
7195
|
}
|
|
6237
7196
|
function relativeStoriesDir(root) {
|
|
6238
|
-
return
|
|
7197
|
+
return path24.relative(root, path24.join(root, ".squad", "stories"));
|
|
6239
7198
|
}
|
|
6240
7199
|
async function pickStory(stories, opts) {
|
|
6241
7200
|
const candidates = opts.all ? stories : stories.filter((s) => !s.planFile);
|
|
@@ -6266,7 +7225,7 @@ async function pickStory(stories, opts) {
|
|
|
6266
7225
|
value: s.intakePath
|
|
6267
7226
|
}))
|
|
6268
7227
|
});
|
|
6269
|
-
return candidates.find((s) =>
|
|
7228
|
+
return candidates.find((s) => path24.resolve(s.intakePath) === path24.resolve(pick));
|
|
6270
7229
|
}
|
|
6271
7230
|
function formatLabel(s) {
|
|
6272
7231
|
const head = `${s.feature} / ${s.id}`;
|
|
@@ -6336,14 +7295,6 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
6336
7295
|
step(`planning ${story.feature} / ${story.id}`);
|
|
6337
7296
|
kv("provider", planner.provider);
|
|
6338
7297
|
kv("model", modelId);
|
|
6339
|
-
kv(
|
|
6340
|
-
"runtime",
|
|
6341
|
-
`${draftRuntime.kind} \xB7 ${draftRuntime.providerName}` + (planner.provider === "anthropic" ? ` (${anthropicRuntimeChoice})` : "")
|
|
6342
|
-
);
|
|
6343
|
-
kv(
|
|
6344
|
-
"budget",
|
|
6345
|
-
`${planner.budget.maxFileReads} reads \xB7 ${(planner.budget.maxContextBytes / 1024).toFixed(0)} KB \xB7 ${planner.budget.maxDurationSeconds}s`
|
|
6346
|
-
);
|
|
6347
7298
|
blank();
|
|
6348
7299
|
const interactive = !opts.yes && Boolean(process.stdin.isTTY);
|
|
6349
7300
|
if (interactive) {
|
|
@@ -6395,64 +7346,27 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
6395
7346
|
const cacheEnabled = planner.cache?.enabled ?? true;
|
|
6396
7347
|
const runId = newRunId();
|
|
6397
7348
|
const bus = new PlannerEventBus();
|
|
7349
|
+
const apiUiCtx = {
|
|
7350
|
+
sessionSpinner,
|
|
7351
|
+
stageLine: { scout: "pending", draft: "pending", validation: "pending" },
|
|
7352
|
+
activePlannerStage: null,
|
|
7353
|
+
thinkingBlockChars: 0,
|
|
7354
|
+
thinkingState: { running: false, blockStartedAt: 0, totalChars: 0, totalDurationMs: 0 },
|
|
7355
|
+
thinkingSpinner: { current: null },
|
|
7356
|
+
thinkingTick: { id: null },
|
|
7357
|
+
budget,
|
|
7358
|
+
budgetCaps: null,
|
|
7359
|
+
startedAt,
|
|
7360
|
+
interactive,
|
|
7361
|
+
stagesIntroPrinted: false,
|
|
7362
|
+
lastToolUi: null,
|
|
7363
|
+
streamedValidationIssues: { count: 0 },
|
|
7364
|
+
validationStreamCappedMsg: false,
|
|
7365
|
+
anthropicRuntimeChoice: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0,
|
|
7366
|
+
usageAcc: { inputTokens: 0, outputTokens: 0 }
|
|
7367
|
+
};
|
|
6398
7368
|
const unsubscribe = bus.subscribe((e) => {
|
|
6399
|
-
|
|
6400
|
-
case "tool_call": {
|
|
6401
|
-
const name = e.toolCall.name;
|
|
6402
|
-
const inp = e.toolCall.input;
|
|
6403
|
-
let label;
|
|
6404
|
-
if (name === "read_file") {
|
|
6405
|
-
label = `read ${String(inp.path ?? "<unknown>")}`;
|
|
6406
|
-
} else if (name === "grep") {
|
|
6407
|
-
label = `grep ${String(inp.pattern ?? "?").slice(0, 48)}`;
|
|
6408
|
-
} else if (name === "list_dir") {
|
|
6409
|
-
label = `list_dir ${String(inp.path ?? "?")}`;
|
|
6410
|
-
} else {
|
|
6411
|
-
label = name;
|
|
6412
|
-
}
|
|
6413
|
-
sessionSpinner.current?.succeed(
|
|
6414
|
-
`${label} (${(e.bytesLoaded / 1024).toFixed(1)} KB \xB7 ${(e.totalBytes / 1024).toFixed(1)} KB / ${(planner.budget.maxContextBytes / 1024).toFixed(0)} KB)`
|
|
6415
|
-
);
|
|
6416
|
-
sessionSpinner.current = spinner("next tool\u2026");
|
|
6417
|
-
break;
|
|
6418
|
-
}
|
|
6419
|
-
case "stage_started": {
|
|
6420
|
-
sessionSpinner.current?.stop();
|
|
6421
|
-
if (e.stage === "scout") sessionSpinner.current = spinner("scouting\u2026");
|
|
6422
|
-
else if (e.stage === "draft") sessionSpinner.current = spinner("drafting\u2026");
|
|
6423
|
-
else if (e.stage === "validation") sessionSpinner.current = spinner("validating\u2026");
|
|
6424
|
-
break;
|
|
6425
|
-
}
|
|
6426
|
-
case "stage_complete": {
|
|
6427
|
-
if (!e.success && e.errorMessage && e.stage === "scout") {
|
|
6428
|
-
sessionSpinner.current?.stop();
|
|
6429
|
-
warning(`scout stage failed: ${e.errorMessage}`);
|
|
6430
|
-
}
|
|
6431
|
-
break;
|
|
6432
|
-
}
|
|
6433
|
-
case "scout_result": {
|
|
6434
|
-
const preview = e.selected.slice(0, 4).join(", ");
|
|
6435
|
-
const more = e.selected.length > 4 ? "\u2026" : "";
|
|
6436
|
-
sessionSpinner.current?.succeed(`scout picked ${e.selected.length} files: ${preview}${more}`);
|
|
6437
|
-
sessionSpinner.current = spinner("drafting\u2026");
|
|
6438
|
-
break;
|
|
6439
|
-
}
|
|
6440
|
-
case "assistant_text":
|
|
6441
|
-
sessionSpinner.current?.succeed("planner thinking complete (this chunk)");
|
|
6442
|
-
sessionSpinner.current = spinner("thinking\u2026");
|
|
6443
|
-
break;
|
|
6444
|
-
case "rate_limit":
|
|
6445
|
-
if (e.phase === "retrying") {
|
|
6446
|
-
sessionSpinner.current?.stop();
|
|
6447
|
-
warning(`${e.provider} rate limit hit \u2014 retrying in ${e.waitSec}s`);
|
|
6448
|
-
sessionSpinner.current = spinner("waiting for rate limit to reset\u2026");
|
|
6449
|
-
}
|
|
6450
|
-
break;
|
|
6451
|
-
case "usage":
|
|
6452
|
-
break;
|
|
6453
|
-
default:
|
|
6454
|
-
break;
|
|
6455
|
-
}
|
|
7369
|
+
dispatchNewPlanApiPlannerEvent(apiUiCtx, e);
|
|
6456
7370
|
});
|
|
6457
7371
|
const decideOnLimit = interactive ? async (ctx) => {
|
|
6458
7372
|
printPlannerLimitExplanation(ctx);
|
|
@@ -6482,6 +7396,7 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
6482
7396
|
decideOnLimit,
|
|
6483
7397
|
events: bus,
|
|
6484
7398
|
runId,
|
|
7399
|
+
paths,
|
|
6485
7400
|
stages: {
|
|
6486
7401
|
scout: {
|
|
6487
7402
|
enabled: scoutEnabled,
|
|
@@ -6496,7 +7411,13 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
6496
7411
|
scoutSystemPrompt
|
|
6497
7412
|
});
|
|
6498
7413
|
} finally {
|
|
7414
|
+
await bus.finalizeEventPersistence?.();
|
|
6499
7415
|
unsubscribe();
|
|
7416
|
+
if (apiUiCtx.thinkingTick.id) {
|
|
7417
|
+
clearInterval(apiUiCtx.thinkingTick.id);
|
|
7418
|
+
apiUiCtx.thinkingTick.id = null;
|
|
7419
|
+
}
|
|
7420
|
+
apiUiCtx.thinkingSpinner.current?.stop();
|
|
6500
7421
|
sessionSpinner.current?.stop();
|
|
6501
7422
|
}
|
|
6502
7423
|
if (!result.planText.trim()) {
|
|
@@ -6531,7 +7452,7 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
6531
7452
|
metadataHeader: header,
|
|
6532
7453
|
partial: !success2
|
|
6533
7454
|
});
|
|
6534
|
-
const relPlan =
|
|
7455
|
+
const relPlan = path24.relative(paths.root, planFile);
|
|
6535
7456
|
try {
|
|
6536
7457
|
await writeLastRun(paths, {
|
|
6537
7458
|
stats: result.stats,
|
|
@@ -6570,11 +7491,27 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
6570
7491
|
});
|
|
6571
7492
|
} catch {
|
|
6572
7493
|
}
|
|
7494
|
+
const stripAnsi2 = (s) => s.replace(/\x1B\[[0-9;]*m/g, "");
|
|
7495
|
+
const budgetCapsForSummary = budget.caps();
|
|
6573
7496
|
blank();
|
|
6574
7497
|
summaryBox(success2 ? " plan generated " : " plan saved (incomplete) ", [
|
|
6575
7498
|
{ key: "file", value: relPlan },
|
|
6576
7499
|
{ key: "nn", value: String(sequenceNumber).padStart(2, "0") },
|
|
6577
7500
|
{ key: "model", value: `${planner.provider}/${modelId}` },
|
|
7501
|
+
{
|
|
7502
|
+
key: "runtime",
|
|
7503
|
+
value: (() => {
|
|
7504
|
+
let chips = "";
|
|
7505
|
+
if (planner.provider === "anthropic") {
|
|
7506
|
+
const d = anthropicProviderSpecific?.draft;
|
|
7507
|
+
if (d?.effort) chips += ` \xB7 effort ${d.effort}`;
|
|
7508
|
+
if (d?.thinking && d.thinking !== "off") chips += ` \xB7 thinking ${d.thinking}`;
|
|
7509
|
+
}
|
|
7510
|
+
const rollup = apiUiCtx.thinkingState.totalChars > 0 ? ` \xB7 \u03A3 ${apiUiCtx.thinkingState.totalChars} thinking chars` : "";
|
|
7511
|
+
return `${draftRuntime.kind} \xB7 ${planner.provider}${chips}${rollup}`;
|
|
7512
|
+
})()
|
|
7513
|
+
},
|
|
7514
|
+
{ key: "events", value: `.squad/runs/${runId}.events.jsonl` },
|
|
6578
7515
|
{
|
|
6579
7516
|
key: "scout",
|
|
6580
7517
|
value: scoutEnabled ? `${result.scout?.selected.length ?? 0} files \xB7 ${result.scout?.tokensUsed ?? 0} tok` : "disabled"
|
|
@@ -6583,7 +7520,19 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
6583
7520
|
key: "validation",
|
|
6584
7521
|
value: validationEnabled ? `${issueCount} issue(s)` : "disabled"
|
|
6585
7522
|
},
|
|
6586
|
-
{
|
|
7523
|
+
{
|
|
7524
|
+
key: "reads",
|
|
7525
|
+
value: stripAnsi2(
|
|
7526
|
+
formatBudgetMeter({
|
|
7527
|
+
reads: snap.reads,
|
|
7528
|
+
readsCap: budgetCapsForSummary.maxFileReads,
|
|
7529
|
+
bytes: snap.bytes,
|
|
7530
|
+
bytesCap: budgetCapsForSummary.maxContextBytes,
|
|
7531
|
+
elapsedMs,
|
|
7532
|
+
durationMsCap: budgetCapsForSummary.maxDurationSeconds * 1e3
|
|
7533
|
+
})
|
|
7534
|
+
)
|
|
7535
|
+
},
|
|
6587
7536
|
{
|
|
6588
7537
|
key: "tokens",
|
|
6589
7538
|
value: `${snap.usage.inputTokens} in \xB7 ${snap.usage.outputTokens} out` + (draftRuntime.kind === "agent-sdk" ? " (agent-sdk: aggregate-only)" : "")
|
|
@@ -6608,14 +7557,19 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
6608
7557
|
]);
|
|
6609
7558
|
if (issueCount > 0 && validationEnabled) {
|
|
6610
7559
|
blank();
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
7560
|
+
const streamed = interactive && apiUiCtx.streamedValidationIssues.count > 0;
|
|
7561
|
+
if (!streamed) {
|
|
7562
|
+
warning(`Validation reported ${issueCount} issue(s) (heuristic; first 10 shown):`);
|
|
7563
|
+
for (const iss of result.validation.issues.slice(0, 10)) {
|
|
7564
|
+
info(
|
|
7565
|
+
` [${iss.severity}] ${iss.kind}${iss.path ? ` ${iss.path}` : ""} \u2014 ${iss.detail}`
|
|
7566
|
+
);
|
|
7567
|
+
}
|
|
7568
|
+
if (issueCount > 10) {
|
|
7569
|
+
info(` (+${issueCount - 10} more \u2014 see metadata header issues=\u2026)`);
|
|
7570
|
+
}
|
|
7571
|
+
} else {
|
|
7572
|
+
warning(`Validation reported ${issueCount} issue(s) (details were streamed during the run).`);
|
|
6619
7573
|
}
|
|
6620
7574
|
if (issueCount > 50) {
|
|
6621
7575
|
warning("Large issue count may mean the validator is misfiring; try `squad new-plan --no-validation` to compare.");
|
|
@@ -6634,14 +7588,14 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
6634
7588
|
info("Note: file-read budget was reached; the model finalised without further reads.");
|
|
6635
7589
|
}
|
|
6636
7590
|
blank();
|
|
6637
|
-
info(`next \u2192 open a new agent chat and attach only ${
|
|
7591
|
+
info(`next \u2192 open a new agent chat and attach only ${path24.basename(planFile)}`);
|
|
6638
7592
|
}
|
|
6639
7593
|
async function emitCopyPrompt(story, paths, config2, clipboard) {
|
|
6640
7594
|
const intakeContent = readFile(story.intakePath);
|
|
6641
7595
|
const composed = buildCopyPlanPromptMarkdown(config2, intakeContent);
|
|
6642
|
-
const promptFile =
|
|
6643
|
-
|
|
6644
|
-
const relPromptFile =
|
|
7596
|
+
const promptFile = path24.join(paths.squadDir, ".last-copy-prompt.md");
|
|
7597
|
+
fs32.writeFileSync(promptFile, composed, "utf8");
|
|
7598
|
+
const relPromptFile = path24.relative(paths.root, promptFile);
|
|
6645
7599
|
const bytes = Buffer.byteLength(composed, "utf8");
|
|
6646
7600
|
const estTokens = Math.round(bytes / 4);
|
|
6647
7601
|
const clipResult = clipboard ? await copyToClipboard(composed) : { ok: false, reason: "clipboard disabled (--no-clipboard)" };
|
|
@@ -6699,7 +7653,7 @@ function printModelBanner(config2) {
|
|
|
6699
7653
|
}
|
|
6700
7654
|
|
|
6701
7655
|
// src/commands/status.ts
|
|
6702
|
-
import
|
|
7656
|
+
import fs33 from "fs";
|
|
6703
7657
|
var kw = 28;
|
|
6704
7658
|
function probeTrackerCredential(config2, overlay) {
|
|
6705
7659
|
const { client, error } = clientFor(config2, overlay);
|
|
@@ -6725,7 +7679,7 @@ async function runStatus() {
|
|
|
6725
7679
|
const scan = scanPlans(paths.plansDir);
|
|
6726
7680
|
const storyCount = listStories(paths).length;
|
|
6727
7681
|
const planCount = scan.usedNumbers.length;
|
|
6728
|
-
const secrets =
|
|
7682
|
+
const secrets = fs33.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
6729
7683
|
const overlay = overlayTrackerEnv(secrets);
|
|
6730
7684
|
banner();
|
|
6731
7685
|
step("squad-kit status");
|
|
@@ -6779,13 +7733,13 @@ function buildTrackerLine(config2, overlay) {
|
|
|
6779
7733
|
}
|
|
6780
7734
|
|
|
6781
7735
|
// src/commands/list.ts
|
|
6782
|
-
import
|
|
7736
|
+
import path25 from "path";
|
|
6783
7737
|
async function runList(opts) {
|
|
6784
7738
|
const root = requireSquadRoot();
|
|
6785
7739
|
const paths = buildPaths(root);
|
|
6786
7740
|
const records = listStories(paths, { feature: opts.feature });
|
|
6787
7741
|
const rows = records.map((r) => {
|
|
6788
|
-
const planAbs = r.planFile ?
|
|
7742
|
+
const planAbs = r.planFile ? path25.join(paths.plansDir, r.feature, r.planFile) : void 0;
|
|
6789
7743
|
const meta = planAbs ? readPlanMetadata(planAbs) : void 0;
|
|
6790
7744
|
return {
|
|
6791
7745
|
feature: r.feature,
|
|
@@ -6824,8 +7778,8 @@ function stripAnsi(s) {
|
|
|
6824
7778
|
}
|
|
6825
7779
|
|
|
6826
7780
|
// src/commands/tracker-link.ts
|
|
6827
|
-
import
|
|
6828
|
-
import
|
|
7781
|
+
import fs34 from "fs";
|
|
7782
|
+
import path26 from "path";
|
|
6829
7783
|
import { input as input4, select as select7 } from "@inquirer/prompts";
|
|
6830
7784
|
async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
6831
7785
|
const root = requireSquadRoot();
|
|
@@ -6872,17 +7826,17 @@ async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
|
6872
7826
|
`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.`
|
|
6873
7827
|
);
|
|
6874
7828
|
}
|
|
6875
|
-
const resolved =
|
|
6876
|
-
const intakeFile = resolved.endsWith("intake.md") ? resolved :
|
|
6877
|
-
if (!
|
|
7829
|
+
const resolved = path26.resolve(resolvedStoryPath);
|
|
7830
|
+
const intakeFile = resolved.endsWith("intake.md") ? resolved : path26.join(resolved, "intake.md");
|
|
7831
|
+
if (!fs34.existsSync(intakeFile)) {
|
|
6878
7832
|
throw new Error(
|
|
6879
7833
|
`intake.md not found at ${intakeFile}. Run \`squad list\` to see valid paths, or \`squad new-story\` to create a story first.`
|
|
6880
7834
|
);
|
|
6881
7835
|
}
|
|
6882
|
-
const content =
|
|
7836
|
+
const content = fs34.readFileSync(intakeFile, "utf8");
|
|
6883
7837
|
const updated = upsertTrackerId(content, resolvedTrackerId);
|
|
6884
|
-
|
|
6885
|
-
success(`Linked tracker id ${resolvedTrackerId} \u2192 ${
|
|
7838
|
+
fs34.writeFileSync(intakeFile, updated, "utf8");
|
|
7839
|
+
success(`Linked tracker id ${resolvedTrackerId} \u2192 ${path26.relative(root, intakeFile)}`);
|
|
6886
7840
|
blank();
|
|
6887
7841
|
step("Next:");
|
|
6888
7842
|
info("1) Review the intake \u2014 the Source block at the top now carries the tracker id.");
|
|
@@ -6900,8 +7854,8 @@ function upsertTrackerId(content, id) {
|
|
|
6900
7854
|
}
|
|
6901
7855
|
|
|
6902
7856
|
// src/commands/doctor-engine.ts
|
|
6903
|
-
import
|
|
6904
|
-
import
|
|
7857
|
+
import fs35 from "fs";
|
|
7858
|
+
import path27 from "path";
|
|
6905
7859
|
async function gatherContext(paths) {
|
|
6906
7860
|
const ctx = { hasLegacyPromptsDir: false };
|
|
6907
7861
|
try {
|
|
@@ -6910,29 +7864,29 @@ async function gatherContext(paths) {
|
|
|
6910
7864
|
ctx.configError = err2;
|
|
6911
7865
|
}
|
|
6912
7866
|
try {
|
|
6913
|
-
ctx.secrets =
|
|
7867
|
+
ctx.secrets = fs35.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
6914
7868
|
} catch (err2) {
|
|
6915
7869
|
ctx.secretsError = err2;
|
|
6916
7870
|
}
|
|
6917
|
-
ctx.hasLegacyPromptsDir =
|
|
7871
|
+
ctx.hasLegacyPromptsDir = fs35.existsSync(paths.promptsDir);
|
|
6918
7872
|
return ctx;
|
|
6919
7873
|
}
|
|
6920
7874
|
function gitignoreHasManagedBlock(repoRoot) {
|
|
6921
|
-
const gitignore =
|
|
6922
|
-
return
|
|
7875
|
+
const gitignore = path27.join(repoRoot, ".gitignore");
|
|
7876
|
+
return fs35.existsSync(gitignore) && fs35.readFileSync(gitignore, "utf8").includes(".squad/secrets.yaml");
|
|
6923
7877
|
}
|
|
6924
7878
|
function gitignoreHasTrashPattern(repoRoot) {
|
|
6925
|
-
const gitignore =
|
|
6926
|
-
return
|
|
7879
|
+
const gitignore = path27.join(repoRoot, ".gitignore");
|
|
7880
|
+
return fs35.existsSync(gitignore) && fs35.readFileSync(gitignore, "utf8").includes(SQUAD_TRASH_PATTERN);
|
|
6927
7881
|
}
|
|
6928
7882
|
async function checkDirStructure(paths, _ctx, fix) {
|
|
6929
|
-
const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !
|
|
7883
|
+
const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !fs35.existsSync(p));
|
|
6930
7884
|
if (need.length === 0) {
|
|
6931
7885
|
return { id: "dirs", name: ".squad/ directory structure", status: "ok" };
|
|
6932
7886
|
}
|
|
6933
7887
|
if (fix) {
|
|
6934
7888
|
for (const p of need) {
|
|
6935
|
-
|
|
7889
|
+
fs35.mkdirSync(p, { recursive: true });
|
|
6936
7890
|
}
|
|
6937
7891
|
return { id: "dirs", name: ".squad/ directory structure", status: "ok", detail: "repaired" };
|
|
6938
7892
|
}
|
|
@@ -6940,7 +7894,7 @@ async function checkDirStructure(paths, _ctx, fix) {
|
|
|
6940
7894
|
id: "dirs",
|
|
6941
7895
|
name: ".squad/ directory structure",
|
|
6942
7896
|
status: "warn",
|
|
6943
|
-
detail: `missing: ${need.map((p) =>
|
|
7897
|
+
detail: `missing: ${need.map((p) => path27.relative(paths.root, p)).join(", ")}`,
|
|
6944
7898
|
fixable: true,
|
|
6945
7899
|
fixHint: "squad doctor --fix"
|
|
6946
7900
|
};
|
|
@@ -6995,15 +7949,15 @@ async function checkSecretsPermissions(paths, _ctx, fix) {
|
|
|
6995
7949
|
if (process.platform === "win32") {
|
|
6996
7950
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "skip", detail: "Windows" };
|
|
6997
7951
|
}
|
|
6998
|
-
if (!
|
|
7952
|
+
if (!fs35.existsSync(paths.secretsFile)) {
|
|
6999
7953
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "not present" };
|
|
7000
7954
|
}
|
|
7001
|
-
const mode =
|
|
7955
|
+
const mode = fs35.statSync(paths.secretsFile).mode & 511;
|
|
7002
7956
|
if (mode === 384) {
|
|
7003
7957
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok" };
|
|
7004
7958
|
}
|
|
7005
7959
|
if (fix) {
|
|
7006
|
-
|
|
7960
|
+
fs35.chmodSync(paths.secretsFile, 384);
|
|
7007
7961
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "repaired" };
|
|
7008
7962
|
}
|
|
7009
7963
|
return {
|
|
@@ -7024,7 +7978,7 @@ async function checkSecretsParseable(_paths, ctx) {
|
|
|
7024
7978
|
detail: ctx.secretsError.message
|
|
7025
7979
|
};
|
|
7026
7980
|
}
|
|
7027
|
-
if (!
|
|
7981
|
+
if (!fs35.existsSync(_paths.secretsFile)) {
|
|
7028
7982
|
return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok", detail: "not present" };
|
|
7029
7983
|
}
|
|
7030
7984
|
return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok" };
|
|
@@ -7578,23 +8532,23 @@ async function runDoctor(opts) {
|
|
|
7578
8532
|
}
|
|
7579
8533
|
|
|
7580
8534
|
// src/commands/migrate.ts
|
|
7581
|
-
import
|
|
7582
|
-
import
|
|
8535
|
+
import fs36 from "fs";
|
|
8536
|
+
import path28 from "path";
|
|
7583
8537
|
import { confirm as confirm7 } from "@inquirer/prompts";
|
|
7584
8538
|
var LEGACY_PROMPTS_MIGRATION = {
|
|
7585
8539
|
id: "legacy-prompts",
|
|
7586
8540
|
name: "Remove legacy .squad/prompts/",
|
|
7587
|
-
isApplied: (paths) => !
|
|
7588
|
-
describe: (paths) => `delete ${
|
|
8541
|
+
isApplied: (paths) => !fs36.existsSync(paths.promptsDir),
|
|
8542
|
+
describe: (paths) => `delete ${path28.relative(paths.root, paths.promptsDir)} (prompts are now bundled in squad-kit)`,
|
|
7589
8543
|
apply: (paths) => {
|
|
7590
8544
|
const target = paths.promptsDir;
|
|
7591
|
-
if (!target.includes(
|
|
8545
|
+
if (!target.includes(path28.sep + ".squad" + path28.sep + "prompts")) {
|
|
7592
8546
|
throw new Error(
|
|
7593
8547
|
`Refusing to delete suspicious path: ${target}. This is a safety check \u2014 run \`squad doctor\` if .squad/ paths look wrong.`
|
|
7594
8548
|
);
|
|
7595
8549
|
}
|
|
7596
|
-
if (
|
|
7597
|
-
|
|
8550
|
+
if (fs36.existsSync(target)) {
|
|
8551
|
+
fs36.rmSync(target, { recursive: true, force: true });
|
|
7598
8552
|
}
|
|
7599
8553
|
}
|
|
7600
8554
|
};
|
|
@@ -7602,8 +8556,8 @@ var GITIGNORE_MIGRATION = {
|
|
|
7602
8556
|
id: "gitignore",
|
|
7603
8557
|
name: "Ensure .gitignore managed block",
|
|
7604
8558
|
isApplied: (paths) => {
|
|
7605
|
-
const f =
|
|
7606
|
-
return
|
|
8559
|
+
const f = path28.join(paths.root, ".gitignore");
|
|
8560
|
+
return fs36.existsSync(f) && fs36.readFileSync(f, "utf8").includes(".squad/secrets.yaml");
|
|
7607
8561
|
},
|
|
7608
8562
|
describe: () => "add squad-kit managed block to .gitignore",
|
|
7609
8563
|
apply: (paths) => {
|
|
@@ -7615,23 +8569,23 @@ var SECRETS_PERMS_MIGRATION = {
|
|
|
7615
8569
|
name: "Tighten .squad/secrets.yaml permissions",
|
|
7616
8570
|
isApplied: (paths) => {
|
|
7617
8571
|
if (process.platform === "win32") return true;
|
|
7618
|
-
if (!
|
|
7619
|
-
const st =
|
|
8572
|
+
if (!fs36.existsSync(paths.secretsFile)) return true;
|
|
8573
|
+
const st = fs36.statSync(paths.secretsFile);
|
|
7620
8574
|
return (st.mode & 511) === 384;
|
|
7621
8575
|
},
|
|
7622
|
-
describe: (paths) => `chmod 0600 ${
|
|
8576
|
+
describe: (paths) => `chmod 0600 ${path28.relative(paths.root, paths.secretsFile)}`,
|
|
7623
8577
|
apply: (paths) => {
|
|
7624
8578
|
if (process.platform === "win32") return;
|
|
7625
|
-
if (
|
|
8579
|
+
if (fs36.existsSync(paths.secretsFile)) fs36.chmodSync(paths.secretsFile, 384);
|
|
7626
8580
|
}
|
|
7627
8581
|
};
|
|
7628
8582
|
var PLANNER_BUDGET_DEFAULTS_MIGRATION = {
|
|
7629
8583
|
id: "planner-budget-defaults",
|
|
7630
8584
|
name: "Backfill planner.budget defaults in config.yaml",
|
|
7631
8585
|
isApplied: (paths) => {
|
|
7632
|
-
if (!
|
|
8586
|
+
if (!fs36.existsSync(paths.configFile)) return true;
|
|
7633
8587
|
try {
|
|
7634
|
-
const raw =
|
|
8588
|
+
const raw = fs36.readFileSync(paths.configFile, "utf8");
|
|
7635
8589
|
if (!/^\s*planner:/m.test(raw)) return true;
|
|
7636
8590
|
if (/^\s*budget:/m.test(raw)) return true;
|
|
7637
8591
|
const cfg = loadConfig(paths.configFile);
|
|
@@ -7650,9 +8604,9 @@ var CONFIG_NORMALIZE_MIGRATION = {
|
|
|
7650
8604
|
id: "config-normalize",
|
|
7651
8605
|
name: "Normalise .squad/config.yaml formatting",
|
|
7652
8606
|
isApplied: (paths) => {
|
|
7653
|
-
if (!
|
|
8607
|
+
if (!fs36.existsSync(paths.configFile)) return true;
|
|
7654
8608
|
try {
|
|
7655
|
-
const raw =
|
|
8609
|
+
const raw = fs36.readFileSync(paths.configFile, "utf8");
|
|
7656
8610
|
const cfg = loadConfig(paths.configFile);
|
|
7657
8611
|
return raw.trim() === serializeConfig(cfg).trim();
|
|
7658
8612
|
} catch {
|
|
@@ -7661,7 +8615,7 @@ var CONFIG_NORMALIZE_MIGRATION = {
|
|
|
7661
8615
|
},
|
|
7662
8616
|
describe: () => "rewrite config.yaml with canonical YAML formatting (drops comments)",
|
|
7663
8617
|
apply: (paths) => {
|
|
7664
|
-
if (!
|
|
8618
|
+
if (!fs36.existsSync(paths.configFile)) return;
|
|
7665
8619
|
const cfg = loadConfig(paths.configFile);
|
|
7666
8620
|
saveConfig(paths.configFile, cfg);
|
|
7667
8621
|
}
|
|
@@ -7742,8 +8696,8 @@ import process3 from "process";
|
|
|
7742
8696
|
import { confirm as confirm8 } from "@inquirer/prompts";
|
|
7743
8697
|
|
|
7744
8698
|
// src/core/package-info.ts
|
|
7745
|
-
import
|
|
7746
|
-
import
|
|
8699
|
+
import fs37 from "fs";
|
|
8700
|
+
import path29 from "path";
|
|
7747
8701
|
function changelogBlobUrl(repositoryUrl, version) {
|
|
7748
8702
|
let base = repositoryUrl;
|
|
7749
8703
|
if (!base) base = "https://github.com/AzmSquad/squad-kit";
|
|
@@ -7754,9 +8708,9 @@ function changelogBlobUrl(repositoryUrl, version) {
|
|
|
7754
8708
|
}
|
|
7755
8709
|
function readInstalledPackage() {
|
|
7756
8710
|
const root = packageRoot();
|
|
7757
|
-
const pkgFile =
|
|
7758
|
-
const pkg = JSON.parse(
|
|
7759
|
-
const isDevInstall =
|
|
8711
|
+
const pkgFile = path29.join(root, "package.json");
|
|
8712
|
+
const pkg = JSON.parse(fs37.readFileSync(pkgFile, "utf8"));
|
|
8713
|
+
const isDevInstall = fs37.existsSync(path29.join(root, "src"));
|
|
7760
8714
|
return {
|
|
7761
8715
|
name: pkg.name,
|
|
7762
8716
|
version: pkg.version,
|
|
@@ -7796,7 +8750,7 @@ function isNewer(a, b) {
|
|
|
7796
8750
|
}
|
|
7797
8751
|
|
|
7798
8752
|
// src/core/package-manager.ts
|
|
7799
|
-
import
|
|
8753
|
+
import path30 from "path";
|
|
7800
8754
|
import process2 from "process";
|
|
7801
8755
|
function detectPackageManager(installRoot) {
|
|
7802
8756
|
const ua = process2.env.npm_config_user_agent;
|
|
@@ -7807,11 +8761,11 @@ function detectPackageManager(installRoot) {
|
|
|
7807
8761
|
if (ua.startsWith("npm/")) return { pm: "npm", reason: "npm_config_user_agent" };
|
|
7808
8762
|
}
|
|
7809
8763
|
const root = installRoot.toLowerCase();
|
|
7810
|
-
if (root.includes(`${
|
|
8764
|
+
if (root.includes(`${path30.sep}pnpm${path30.sep}`) || root.includes(`${path30.sep}.pnpm${path30.sep}`))
|
|
7811
8765
|
return { pm: "pnpm", reason: `install path contains pnpm` };
|
|
7812
|
-
if (root.includes(`${
|
|
8766
|
+
if (root.includes(`${path30.sep}.yarn${path30.sep}`) || root.includes(`${path30.sep}yarn${path30.sep}`))
|
|
7813
8767
|
return { pm: "yarn", reason: `install path contains yarn` };
|
|
7814
|
-
if (root.includes(`${
|
|
8768
|
+
if (root.includes(`${path30.sep}.bun${path30.sep}`) || root.includes(`${path30.sep}bun${path30.sep}`))
|
|
7815
8769
|
return { pm: "bun", reason: `install path contains bun` };
|
|
7816
8770
|
return { pm: "npm", reason: "default fallback" };
|
|
7817
8771
|
}
|
|
@@ -7924,14 +8878,14 @@ import { cors } from "hono/cors";
|
|
|
7924
8878
|
import { secureHeaders } from "hono/secure-headers";
|
|
7925
8879
|
|
|
7926
8880
|
// src/console/api/stories.ts
|
|
7927
|
-
import
|
|
8881
|
+
import fs38 from "fs";
|
|
7928
8882
|
import { z as z6 } from "zod";
|
|
7929
8883
|
|
|
7930
8884
|
// src/utils/path-safety.ts
|
|
7931
|
-
import
|
|
8885
|
+
import path31 from "path";
|
|
7932
8886
|
function isInside(abs, parent) {
|
|
7933
|
-
const a =
|
|
7934
|
-
const p =
|
|
8887
|
+
const a = path31.resolve(abs) + path31.sep;
|
|
8888
|
+
const p = path31.resolve(parent) + path31.sep;
|
|
7935
8889
|
return a.startsWith(p);
|
|
7936
8890
|
}
|
|
7937
8891
|
|
|
@@ -7971,7 +8925,7 @@ function mountStoriesApi(app, opts) {
|
|
|
7971
8925
|
if (!isInside(hit.intakePath, opts.paths.storiesDir)) return c.json({ error: "forbidden" }, 403);
|
|
7972
8926
|
let intakeContent = "";
|
|
7973
8927
|
try {
|
|
7974
|
-
intakeContent =
|
|
8928
|
+
intakeContent = fs38.readFileSync(hit.intakePath, "utf8");
|
|
7975
8929
|
} catch {
|
|
7976
8930
|
}
|
|
7977
8931
|
return c.json({
|
|
@@ -8015,7 +8969,7 @@ function mountStoriesApi(app, opts) {
|
|
|
8015
8969
|
const hit = stories.find((s) => s.id === id);
|
|
8016
8970
|
if (!hit) return c.json({ error: "not_found" }, 404);
|
|
8017
8971
|
if (!isInside(hit.intakePath, opts.paths.storiesDir)) return c.json({ error: "forbidden" }, 403);
|
|
8018
|
-
|
|
8972
|
+
fs38.writeFileSync(hit.intakePath, body.data.intakeContent, "utf8");
|
|
8019
8973
|
return c.json({ ok: true });
|
|
8020
8974
|
});
|
|
8021
8975
|
app.delete("/api/stories/:feature/:id", (c) => {
|
|
@@ -8032,8 +8986,8 @@ function mountStoriesApi(app, opts) {
|
|
|
8032
8986
|
}
|
|
8033
8987
|
|
|
8034
8988
|
// src/console/api/plans.ts
|
|
8035
|
-
import
|
|
8036
|
-
import
|
|
8989
|
+
import fs39 from "fs";
|
|
8990
|
+
import path32 from "path";
|
|
8037
8991
|
import { z as z7 } from "zod";
|
|
8038
8992
|
import { diffLines } from "diff";
|
|
8039
8993
|
var DiffQuery = z7.object({
|
|
@@ -8048,11 +9002,11 @@ function mountPlansApi(app, opts) {
|
|
|
8048
9002
|
app.get("/api/plans", (c) => {
|
|
8049
9003
|
const out = [];
|
|
8050
9004
|
for (const feature of listPlanFeatureNames(opts.paths)) {
|
|
8051
|
-
const featureDir =
|
|
8052
|
-
if (!
|
|
8053
|
-
for (const entry of
|
|
9005
|
+
const featureDir = path32.join(opts.paths.plansDir, feature);
|
|
9006
|
+
if (!fs39.existsSync(featureDir)) continue;
|
|
9007
|
+
for (const entry of fs39.readdirSync(featureDir)) {
|
|
8054
9008
|
if (!entry.endsWith(".md") || entry === "00-index.md") continue;
|
|
8055
|
-
const abs =
|
|
9009
|
+
const abs = path32.join(featureDir, entry);
|
|
8056
9010
|
out.push({ feature, planFile: entry, metadata: readPlanMetadata(abs) });
|
|
8057
9011
|
}
|
|
8058
9012
|
}
|
|
@@ -8061,10 +9015,10 @@ function mountPlansApi(app, opts) {
|
|
|
8061
9015
|
app.get("/api/plans/:feature/:planFile", (c) => {
|
|
8062
9016
|
const { feature, planFile } = c.req.param();
|
|
8063
9017
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
8064
|
-
const abs =
|
|
9018
|
+
const abs = path32.resolve(path32.join(opts.paths.plansDir, feature, planFile));
|
|
8065
9019
|
if (!isInside(abs, opts.paths.plansDir)) return c.json({ error: "forbidden" }, 403);
|
|
8066
|
-
if (!
|
|
8067
|
-
const content =
|
|
9020
|
+
if (!fs39.existsSync(abs)) return c.json({ error: "not_found" }, 404);
|
|
9021
|
+
const content = fs39.readFileSync(abs, "utf8");
|
|
8068
9022
|
return c.json({
|
|
8069
9023
|
feature,
|
|
8070
9024
|
planFile,
|
|
@@ -8082,15 +9036,15 @@ function mountPlansApi(app, opts) {
|
|
|
8082
9036
|
if (!parse.success) return c.json({ error: "invalid_query", issues: parse.error.issues }, 400);
|
|
8083
9037
|
const { feature, a, b } = parse.data;
|
|
8084
9038
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
8085
|
-
const featureDir =
|
|
8086
|
-
const aAbs =
|
|
8087
|
-
const bAbs =
|
|
9039
|
+
const featureDir = path32.join(opts.paths.plansDir, feature);
|
|
9040
|
+
const aAbs = path32.join(featureDir, a);
|
|
9041
|
+
const bAbs = path32.join(featureDir, b);
|
|
8088
9042
|
if (!isInside(aAbs, opts.paths.plansDir) || !isInside(bAbs, opts.paths.plansDir)) {
|
|
8089
9043
|
return c.json({ error: "forbidden" }, 403);
|
|
8090
9044
|
}
|
|
8091
|
-
if (!
|
|
8092
|
-
const aText =
|
|
8093
|
-
const bText =
|
|
9045
|
+
if (!fs39.existsSync(aAbs) || !fs39.existsSync(bAbs)) return c.json({ error: "not_found" }, 404);
|
|
9046
|
+
const aText = fs39.readFileSync(aAbs, "utf8");
|
|
9047
|
+
const bText = fs39.readFileSync(bAbs, "utf8");
|
|
8094
9048
|
const changes = diffLines(aText, bText);
|
|
8095
9049
|
return c.json({ feature, a, b, changes });
|
|
8096
9050
|
});
|
|
@@ -8098,16 +9052,16 @@ function mountPlansApi(app, opts) {
|
|
|
8098
9052
|
const trash = c.req.query("trash") === "1";
|
|
8099
9053
|
const { feature, planFile } = c.req.param();
|
|
8100
9054
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
8101
|
-
const abs =
|
|
9055
|
+
const abs = path32.join(opts.paths.plansDir, feature, planFile);
|
|
8102
9056
|
if (!isInside(abs, opts.paths.plansDir)) return c.json({ error: "forbidden" }, 403);
|
|
8103
|
-
if (!
|
|
9057
|
+
if (!fs39.existsSync(abs)) return c.json({ error: "not_found" }, 404);
|
|
8104
9058
|
if (trash) {
|
|
8105
9059
|
const result = trashOrDelete([abs], opts.paths.trashDir, true);
|
|
8106
|
-
removeOverviewRowForPlanFile(
|
|
9060
|
+
removeOverviewRowForPlanFile(path32.join(opts.paths.plansDir, feature), planFile);
|
|
8107
9061
|
return c.json({ ok: true, trashed: result.trashed });
|
|
8108
9062
|
}
|
|
8109
|
-
|
|
8110
|
-
removeOverviewRowForPlanFile(
|
|
9063
|
+
fs39.rmSync(abs);
|
|
9064
|
+
removeOverviewRowForPlanFile(path32.join(opts.paths.plansDir, feature), planFile);
|
|
8111
9065
|
return c.json({ ok: true });
|
|
8112
9066
|
});
|
|
8113
9067
|
}
|
|
@@ -8155,10 +9109,10 @@ function mountMetaApi(app, opts) {
|
|
|
8155
9109
|
// src/console/api/runs.ts
|
|
8156
9110
|
import { streamSSE } from "hono/streaming";
|
|
8157
9111
|
import { z as z9 } from "zod";
|
|
8158
|
-
import
|
|
8159
|
-
import
|
|
9112
|
+
import path33 from "path";
|
|
9113
|
+
import fs40 from "fs";
|
|
8160
9114
|
var active = /* @__PURE__ */ new Map();
|
|
8161
|
-
var EVENT_BUF_CAP =
|
|
9115
|
+
var EVENT_BUF_CAP = 4096;
|
|
8162
9116
|
var StartBody = z9.object({
|
|
8163
9117
|
feature: z9.string().min(1),
|
|
8164
9118
|
storyId: z9.string().min(1)
|
|
@@ -8269,147 +9223,189 @@ function mountRunsApi(app, opts) {
|
|
|
8269
9223
|
repoMap
|
|
8270
9224
|
});
|
|
8271
9225
|
const userPrompt = composeUserPrompt({
|
|
8272
|
-
intakeContent:
|
|
9226
|
+
intakeContent: fs40.readFileSync(story.intakePath, "utf8")
|
|
8273
9227
|
});
|
|
8274
9228
|
let result;
|
|
8275
9229
|
try {
|
|
8276
|
-
|
|
8277
|
-
|
|
8278
|
-
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
|
|
8290
|
-
|
|
9230
|
+
try {
|
|
9231
|
+
result = await runPlanner({
|
|
9232
|
+
root: opts.paths.root,
|
|
9233
|
+
runtime: draftRuntime,
|
|
9234
|
+
provider: planner.provider,
|
|
9235
|
+
modelId,
|
|
9236
|
+
anthropicProviderSpecific,
|
|
9237
|
+
systemPrompt,
|
|
9238
|
+
userPrompt,
|
|
9239
|
+
budget,
|
|
9240
|
+
maxOutputTokens: planner.maxOutputTokens,
|
|
9241
|
+
cacheEnabled,
|
|
9242
|
+
events: bus,
|
|
9243
|
+
runId,
|
|
9244
|
+
paths: opts.paths,
|
|
9245
|
+
abort: controller.signal,
|
|
9246
|
+
stages: {
|
|
9247
|
+
scout: {
|
|
9248
|
+
enabled: scoutEnabled,
|
|
9249
|
+
runtime: scoutRuntime,
|
|
9250
|
+
modelId: scoutModelId,
|
|
9251
|
+
maxFiles: maxScoutFiles,
|
|
9252
|
+
maxOutputTokens: planner.stages?.scout?.maxOutputTokens ?? 2048
|
|
9253
|
+
}
|
|
9254
|
+
},
|
|
9255
|
+
validation: { enabled: validationEnabled, strict: strictValidation },
|
|
9256
|
+
toolsEnabled: planner.tools,
|
|
9257
|
+
scoutSystemPrompt
|
|
9258
|
+
});
|
|
9259
|
+
} catch (err2) {
|
|
9260
|
+
const durationMs = Date.now() - startedAt;
|
|
9261
|
+
const last = eventBuffer[eventBuffer.length - 1];
|
|
9262
|
+
const isRateLimitAbort = last?.kind === "rate_limit" && last.phase === "aborted";
|
|
9263
|
+
if (!isRateLimitAbort) {
|
|
9264
|
+
bus.emit({ kind: "error", runId, message: err2.message });
|
|
9265
|
+
}
|
|
9266
|
+
bus.emit({
|
|
9267
|
+
kind: "done",
|
|
9268
|
+
runId,
|
|
9269
|
+
success: false,
|
|
9270
|
+
planFile: null,
|
|
9271
|
+
partial: true,
|
|
9272
|
+
stats: emptyStats(durationMs),
|
|
9273
|
+
durationMs
|
|
9274
|
+
});
|
|
9275
|
+
return;
|
|
9276
|
+
}
|
|
9277
|
+
const issueCount = result.validation?.issues.length ?? 0;
|
|
9278
|
+
const issuesByKind = summariseIssuesByKind(result.validation?.issues ?? []);
|
|
9279
|
+
const validationBlocks = strictValidation && validationEnabled && issueCount > 0;
|
|
9280
|
+
const runSuccess = result.finishedNormally && !result.timedOut && !result.userCancelled && !validationBlocks;
|
|
9281
|
+
let planFile = null;
|
|
9282
|
+
if (result.planText.trim()) {
|
|
9283
|
+
const snap = budget.snapshot();
|
|
9284
|
+
const header = buildMetadataHeader({
|
|
9285
|
+
provider: planner.provider,
|
|
9286
|
+
model: modelId,
|
|
9287
|
+
reads: snap.reads,
|
|
9288
|
+
bytes: snap.bytes,
|
|
9289
|
+
inputTokens: snap.usage.inputTokens,
|
|
9290
|
+
outputTokens: snap.usage.outputTokens,
|
|
9291
|
+
durationMs: Date.now() - startedAt,
|
|
9292
|
+
planStatus: runSuccess ? void 0 : "partial",
|
|
9293
|
+
scoutEnabled,
|
|
9294
|
+
validationEnabled,
|
|
9295
|
+
validationIssueCount: issueCount
|
|
9296
|
+
});
|
|
9297
|
+
const out = writePlanFile({
|
|
9298
|
+
paths: opts.paths,
|
|
9299
|
+
config: cfgFresh,
|
|
9300
|
+
story,
|
|
9301
|
+
planBodyMarkdown: result.planText,
|
|
9302
|
+
metadataHeader: header,
|
|
9303
|
+
partial: !runSuccess
|
|
9304
|
+
});
|
|
9305
|
+
planFile = path33.relative(opts.paths.root, out.planFile);
|
|
9306
|
+
}
|
|
9307
|
+
try {
|
|
9308
|
+
await writeLastRun(opts.paths, {
|
|
9309
|
+
stats: result.stats,
|
|
9310
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9311
|
+
provider: planner.provider,
|
|
9312
|
+
model: modelId
|
|
9313
|
+
});
|
|
9314
|
+
await appendRun(opts.paths, {
|
|
9315
|
+
runId,
|
|
9316
|
+
provider: planner.provider,
|
|
9317
|
+
model: modelId,
|
|
9318
|
+
feature: story.feature,
|
|
9319
|
+
storyId: story.id,
|
|
9320
|
+
startedAt: new Date(startedAt).toISOString(),
|
|
9321
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9322
|
+
success: runSuccess,
|
|
9323
|
+
partial: !runSuccess,
|
|
9324
|
+
planFile,
|
|
9325
|
+
stats: result.stats,
|
|
9326
|
+
cacheEnabled,
|
|
9327
|
+
durationMs: Date.now() - startedAt,
|
|
8291
9328
|
scout: {
|
|
8292
9329
|
enabled: scoutEnabled,
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
|
|
8296
|
-
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
if (!isRateLimitAbort) {
|
|
8308
|
-
bus.emit({ kind: "error", runId, message: err2.message });
|
|
9330
|
+
selectedCount: result.scout?.selected.length,
|
|
9331
|
+
tokensUsed: result.scout?.tokensUsed,
|
|
9332
|
+
durationMs: result.scout?.durationMs
|
|
9333
|
+
},
|
|
9334
|
+
validation: {
|
|
9335
|
+
enabled: validationEnabled,
|
|
9336
|
+
issuesCount: issueCount,
|
|
9337
|
+
issuesByKind,
|
|
9338
|
+
durationMs: result.validation?.durationMs
|
|
9339
|
+
},
|
|
9340
|
+
plannerRuntime: { kind: draftRuntime.kind, provider: planner.provider },
|
|
9341
|
+
providerOptionsSnapshot: anthropicProviderSpecific ? { anthropic: anthropicProviderSpecific } : void 0
|
|
9342
|
+
});
|
|
9343
|
+
} catch {
|
|
8309
9344
|
}
|
|
8310
9345
|
bus.emit({
|
|
8311
9346
|
kind: "done",
|
|
8312
9347
|
runId,
|
|
8313
|
-
success: false,
|
|
8314
|
-
planFile: null,
|
|
8315
|
-
partial: true,
|
|
8316
|
-
stats: emptyStats(durationMs),
|
|
8317
|
-
durationMs
|
|
8318
|
-
});
|
|
8319
|
-
return;
|
|
8320
|
-
}
|
|
8321
|
-
const issueCount = result.validation?.issues.length ?? 0;
|
|
8322
|
-
const issuesByKind = summariseIssuesByKind(result.validation?.issues ?? []);
|
|
8323
|
-
const validationBlocks = strictValidation && validationEnabled && issueCount > 0;
|
|
8324
|
-
const runSuccess = result.finishedNormally && !result.timedOut && !result.userCancelled && !validationBlocks;
|
|
8325
|
-
let planFile = null;
|
|
8326
|
-
if (result.planText.trim()) {
|
|
8327
|
-
const snap = budget.snapshot();
|
|
8328
|
-
const header = buildMetadataHeader({
|
|
8329
|
-
provider: planner.provider,
|
|
8330
|
-
model: modelId,
|
|
8331
|
-
reads: snap.reads,
|
|
8332
|
-
bytes: snap.bytes,
|
|
8333
|
-
inputTokens: snap.usage.inputTokens,
|
|
8334
|
-
outputTokens: snap.usage.outputTokens,
|
|
8335
|
-
durationMs: Date.now() - startedAt,
|
|
8336
|
-
planStatus: runSuccess ? void 0 : "partial",
|
|
8337
|
-
scoutEnabled,
|
|
8338
|
-
validationEnabled,
|
|
8339
|
-
validationIssueCount: issueCount
|
|
8340
|
-
});
|
|
8341
|
-
const out = writePlanFile({
|
|
8342
|
-
paths: opts.paths,
|
|
8343
|
-
config: cfgFresh,
|
|
8344
|
-
story,
|
|
8345
|
-
planBodyMarkdown: result.planText,
|
|
8346
|
-
metadataHeader: header,
|
|
8347
|
-
partial: !runSuccess
|
|
8348
|
-
});
|
|
8349
|
-
planFile = path32.relative(opts.paths.root, out.planFile);
|
|
8350
|
-
}
|
|
8351
|
-
try {
|
|
8352
|
-
await writeLastRun(opts.paths, {
|
|
8353
|
-
stats: result.stats,
|
|
8354
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8355
|
-
provider: planner.provider,
|
|
8356
|
-
model: modelId
|
|
8357
|
-
});
|
|
8358
|
-
await appendRun(opts.paths, {
|
|
8359
|
-
runId,
|
|
8360
|
-
provider: planner.provider,
|
|
8361
|
-
model: modelId,
|
|
8362
|
-
feature: story.feature,
|
|
8363
|
-
storyId: story.id,
|
|
8364
|
-
startedAt: new Date(startedAt).toISOString(),
|
|
8365
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8366
9348
|
success: runSuccess,
|
|
8367
|
-
partial: !runSuccess,
|
|
8368
9349
|
planFile,
|
|
9350
|
+
partial: !runSuccess,
|
|
8369
9351
|
stats: result.stats,
|
|
8370
|
-
|
|
8371
|
-
durationMs: Date.now() - startedAt,
|
|
8372
|
-
scout: {
|
|
8373
|
-
enabled: scoutEnabled,
|
|
8374
|
-
selectedCount: result.scout?.selected.length,
|
|
8375
|
-
tokensUsed: result.scout?.tokensUsed,
|
|
8376
|
-
durationMs: result.scout?.durationMs
|
|
8377
|
-
},
|
|
8378
|
-
validation: {
|
|
8379
|
-
enabled: validationEnabled,
|
|
8380
|
-
issuesCount: issueCount,
|
|
8381
|
-
issuesByKind,
|
|
8382
|
-
durationMs: result.validation?.durationMs
|
|
8383
|
-
},
|
|
8384
|
-
plannerRuntime: { kind: draftRuntime.kind, provider: planner.provider },
|
|
8385
|
-
providerOptionsSnapshot: anthropicProviderSpecific ? { anthropic: anthropicProviderSpecific } : void 0
|
|
9352
|
+
durationMs: Date.now() - startedAt
|
|
8386
9353
|
});
|
|
8387
|
-
}
|
|
9354
|
+
} finally {
|
|
9355
|
+
await bus.finalizeEventPersistence?.();
|
|
8388
9356
|
}
|
|
8389
|
-
bus.emit({
|
|
8390
|
-
kind: "done",
|
|
8391
|
-
runId,
|
|
8392
|
-
success: runSuccess,
|
|
8393
|
-
planFile,
|
|
8394
|
-
partial: !runSuccess,
|
|
8395
|
-
stats: result.stats,
|
|
8396
|
-
durationMs: Date.now() - startedAt
|
|
8397
|
-
});
|
|
8398
9357
|
})().finally(() => {
|
|
8399
9358
|
active.delete(runId);
|
|
8400
9359
|
});
|
|
8401
9360
|
active.set(runId, { runId, bus, controller, feature: story.feature, storyId: story.id, startedAt, done, eventBuffer });
|
|
8402
9361
|
return c.json({ runId, eventStream: `/api/runs/${runId}/stream` }, 202);
|
|
8403
9362
|
});
|
|
9363
|
+
app.get("/api/runs/:runId/events", async (c) => {
|
|
9364
|
+
const runId = c.req.param("runId");
|
|
9365
|
+
const fromQ = Number(c.req.query("from") ?? 0);
|
|
9366
|
+
const fromIndex = Number.isFinite(fromQ) ? Math.max(0, Math.floor(fromQ)) : 0;
|
|
9367
|
+
const limQ = Number(c.req.query("limit") ?? 1e3);
|
|
9368
|
+
const limit = Math.min(
|
|
9369
|
+
2e3,
|
|
9370
|
+
Math.max(1, Number.isFinite(limQ) ? Math.floor(limQ) : 1e3)
|
|
9371
|
+
);
|
|
9372
|
+
const reader = await openRunEventsReader(opts.paths, runId);
|
|
9373
|
+
if (!reader) return c.json({ error: "not_found" }, 404);
|
|
9374
|
+
const events = [];
|
|
9375
|
+
for await (const e of reader.iterate({ fromIndex, limit })) events.push(e);
|
|
9376
|
+
const total = await reader.count();
|
|
9377
|
+
return c.json({ runId, fromIndex, limit, total, events });
|
|
9378
|
+
});
|
|
9379
|
+
app.get("/api/runs/:runId", async (c) => {
|
|
9380
|
+
const runId = c.req.param("runId");
|
|
9381
|
+
const all = await listRuns(opts.paths);
|
|
9382
|
+
const rec = all.find((r) => r.runId === runId);
|
|
9383
|
+
if (!rec) return c.json({ error: "not_found" }, 404);
|
|
9384
|
+
return c.json(rec);
|
|
9385
|
+
});
|
|
8404
9386
|
app.get("/api/runs/:runId/stream", (c) => {
|
|
8405
9387
|
const runId = c.req.param("runId");
|
|
8406
9388
|
const run = active.get(runId);
|
|
9389
|
+
if (!run) {
|
|
9390
|
+
return streamSSE(c, async (stream) => {
|
|
9391
|
+
const reader = await openRunEventsReader(opts.paths, runId);
|
|
9392
|
+
if (!reader) {
|
|
9393
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ message: "unknown run" }) });
|
|
9394
|
+
await stream.writeSSE({ event: "closed", data: "{}" });
|
|
9395
|
+
return;
|
|
9396
|
+
}
|
|
9397
|
+
try {
|
|
9398
|
+
for await (const e of reader.iterate()) {
|
|
9399
|
+
await stream.writeSSE({ event: e.kind, data: JSON.stringify(e) });
|
|
9400
|
+
}
|
|
9401
|
+
await stream.writeSSE({ event: "closed", data: "{}" });
|
|
9402
|
+
} catch {
|
|
9403
|
+
await stream.writeSSE({ event: "error", data: JSON.stringify({ message: "replay_failed" }) });
|
|
9404
|
+
await stream.writeSSE({ event: "closed", data: "{}" });
|
|
9405
|
+
}
|
|
9406
|
+
});
|
|
9407
|
+
}
|
|
8407
9408
|
return streamSSE(c, async (stream) => {
|
|
8408
|
-
if (!run) {
|
|
8409
|
-
await stream.writeSSE({ event: "error", data: JSON.stringify({ message: "unknown run" }) });
|
|
8410
|
-
await stream.writeSSE({ event: "closed", data: "{}" });
|
|
8411
|
-
return;
|
|
8412
|
-
}
|
|
8413
9409
|
let idx = 0;
|
|
8414
9410
|
let resolveWaiter = null;
|
|
8415
9411
|
const notify = () => resolveWaiter?.();
|
|
@@ -8698,17 +9694,17 @@ function countStories(stories) {
|
|
|
8698
9694
|
}
|
|
8699
9695
|
|
|
8700
9696
|
// src/console/api/projects.ts
|
|
8701
|
-
import
|
|
8702
|
-
import
|
|
9697
|
+
import fs41 from "fs/promises";
|
|
9698
|
+
import path34 from "path";
|
|
8703
9699
|
import os from "os";
|
|
8704
9700
|
var MAX = 10;
|
|
8705
9701
|
function recentFilePath() {
|
|
8706
|
-
return
|
|
9702
|
+
return path34.join(os.homedir(), ".squad", "recent-projects.json");
|
|
8707
9703
|
}
|
|
8708
9704
|
async function loadRecent() {
|
|
8709
9705
|
const RECENT_FILE = recentFilePath();
|
|
8710
9706
|
try {
|
|
8711
|
-
const raw = await
|
|
9707
|
+
const raw = await fs41.readFile(RECENT_FILE, "utf8");
|
|
8712
9708
|
const parsed = JSON.parse(raw);
|
|
8713
9709
|
return Array.isArray(parsed) ? parsed.slice(0, MAX) : [];
|
|
8714
9710
|
} catch {
|
|
@@ -8717,8 +9713,8 @@ async function loadRecent() {
|
|
|
8717
9713
|
}
|
|
8718
9714
|
async function saveRecent(list) {
|
|
8719
9715
|
const RECENT_FILE = recentFilePath();
|
|
8720
|
-
await
|
|
8721
|
-
await
|
|
9716
|
+
await fs41.mkdir(path34.dirname(RECENT_FILE), { recursive: true });
|
|
9717
|
+
await fs41.writeFile(RECENT_FILE, JSON.stringify(list.slice(0, MAX), null, 2) + "\n", "utf8");
|
|
8722
9718
|
}
|
|
8723
9719
|
function mountProjectsApi(app, opts) {
|
|
8724
9720
|
app.get("/api/projects/recent", async (c) => c.json(await loadRecent()));
|
|
@@ -8747,8 +9743,8 @@ function mountApi(app, opts) {
|
|
|
8747
9743
|
}
|
|
8748
9744
|
|
|
8749
9745
|
// src/console/static-assets.ts
|
|
8750
|
-
import
|
|
8751
|
-
import
|
|
9746
|
+
import fs42 from "fs";
|
|
9747
|
+
import path35 from "path";
|
|
8752
9748
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8753
9749
|
import { Readable } from "stream";
|
|
8754
9750
|
function pathnameForStatic(c) {
|
|
@@ -8778,18 +9774,18 @@ function mountStaticAssets(app, _opts) {
|
|
|
8778
9774
|
}
|
|
8779
9775
|
if (c.req.path.startsWith("/api/") || c.req.path.startsWith("/healthz")) return c.notFound();
|
|
8780
9776
|
if (!distDir) return c.text("UI not built. Run `pnpm -C console-ui build`.", 404);
|
|
8781
|
-
const indexHtml =
|
|
8782
|
-
if (!
|
|
8783
|
-
return c.html(
|
|
9777
|
+
const indexHtml = path35.join(distDir, "index.html");
|
|
9778
|
+
if (!fs42.existsSync(indexHtml)) return c.text("UI not built", 404);
|
|
9779
|
+
return c.html(fs42.readFileSync(indexHtml, "utf8"), 200, { "cache-control": "no-store" });
|
|
8784
9780
|
});
|
|
8785
9781
|
}
|
|
8786
9782
|
function assetResponse(c, distDir, pathname) {
|
|
8787
9783
|
if (!distDir) return c.text("UI not built", 404);
|
|
8788
9784
|
const rel = pathname.replace(/^\//, "");
|
|
8789
|
-
const abs =
|
|
9785
|
+
const abs = path35.normalize(path35.join(distDir, rel));
|
|
8790
9786
|
if (!isInside2(abs, distDir)) return c.text("forbidden", 403);
|
|
8791
|
-
if (!
|
|
8792
|
-
const body = Readable.toWeb(
|
|
9787
|
+
if (!fs42.existsSync(abs) || !fs42.statSync(abs).isFile()) return c.text("not found", 404);
|
|
9788
|
+
const body = Readable.toWeb(fs42.createReadStream(abs));
|
|
8793
9789
|
return new Response(body, {
|
|
8794
9790
|
headers: {
|
|
8795
9791
|
"content-type": contentTypeFor(abs),
|
|
@@ -8799,9 +9795,9 @@ function assetResponse(c, distDir, pathname) {
|
|
|
8799
9795
|
}
|
|
8800
9796
|
function serveFile(c, distDir, name, ct) {
|
|
8801
9797
|
if (!distDir) return c.text("UI not built", 404);
|
|
8802
|
-
const abs =
|
|
8803
|
-
if (!
|
|
8804
|
-
return new Response(
|
|
9798
|
+
const abs = path35.join(distDir, name);
|
|
9799
|
+
if (!fs42.existsSync(abs)) return c.text("not found", 404);
|
|
9800
|
+
return new Response(fs42.readFileSync(abs), {
|
|
8805
9801
|
headers: { "content-type": ct, "cache-control": "public, max-age=86400" }
|
|
8806
9802
|
});
|
|
8807
9803
|
}
|
|
@@ -8816,13 +9812,13 @@ function contentTypeFor(p) {
|
|
|
8816
9812
|
return "application/octet-stream";
|
|
8817
9813
|
}
|
|
8818
9814
|
function isInside2(abs, parent) {
|
|
8819
|
-
const a =
|
|
8820
|
-
const p =
|
|
9815
|
+
const a = path35.resolve(abs) + path35.sep;
|
|
9816
|
+
const p = path35.resolve(parent) + path35.sep;
|
|
8821
9817
|
return a.startsWith(p);
|
|
8822
9818
|
}
|
|
8823
9819
|
function resolveConsoleUiDist() {
|
|
8824
9820
|
const fromEnv = process.env.SQUAD_CONSOLE_UI_DIST;
|
|
8825
|
-
if (fromEnv &&
|
|
9821
|
+
if (fromEnv && fs42.existsSync(path35.join(fromEnv, "index.html"))) return fromEnv;
|
|
8826
9822
|
const candidates = [
|
|
8827
9823
|
fileURLToPath2(new URL("./console-ui/", import.meta.url)),
|
|
8828
9824
|
// dist/cli.js → dist/console-ui/
|
|
@@ -8834,7 +9830,7 @@ function resolveConsoleUiDist() {
|
|
|
8834
9830
|
// src/console/*.ts → project/console-ui/dist
|
|
8835
9831
|
];
|
|
8836
9832
|
for (const c of candidates) {
|
|
8837
|
-
if (
|
|
9833
|
+
if (fs42.existsSync(path35.join(c, "index.html"))) return c;
|
|
8838
9834
|
}
|
|
8839
9835
|
return null;
|
|
8840
9836
|
}
|
|
@@ -9016,18 +10012,18 @@ async function runConsole(opts) {
|
|
|
9016
10012
|
}
|
|
9017
10013
|
|
|
9018
10014
|
// src/commands/rm/story.ts
|
|
9019
|
-
import
|
|
9020
|
-
import
|
|
10015
|
+
import fs44 from "fs";
|
|
10016
|
+
import path37 from "path";
|
|
9021
10017
|
import { confirm as confirm10 } from "@inquirer/prompts";
|
|
9022
10018
|
|
|
9023
10019
|
// src/commands/rm/feature.ts
|
|
9024
|
-
import
|
|
9025
|
-
import
|
|
10020
|
+
import fs43 from "fs";
|
|
10021
|
+
import path36 from "path";
|
|
9026
10022
|
import { confirm as confirm9 } from "@inquirer/prompts";
|
|
9027
10023
|
function countStoryDirs(storiesDir, feature) {
|
|
9028
|
-
const d =
|
|
9029
|
-
if (!
|
|
9030
|
-
return
|
|
10024
|
+
const d = path36.join(storiesDir, feature);
|
|
10025
|
+
if (!fs43.existsSync(d)) return 0;
|
|
10026
|
+
return fs43.readdirSync(d, { withFileTypes: true }).filter((x) => x.isDirectory()).length;
|
|
9031
10027
|
}
|
|
9032
10028
|
async function runRmFeature(featureSlug, opts = {}) {
|
|
9033
10029
|
const root = requireSquadRoot();
|
|
@@ -9061,17 +10057,17 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
9061
10057
|
feature = await pickFeatureInteractive(paths);
|
|
9062
10058
|
}
|
|
9063
10059
|
}
|
|
9064
|
-
const storyDir =
|
|
9065
|
-
const planDir =
|
|
10060
|
+
const storyDir = path36.join(paths.storiesDir, feature);
|
|
10061
|
+
const planDir = path36.join(paths.plansDir, feature);
|
|
9066
10062
|
const nStories = countStoryDirs(paths.storiesDir, feature);
|
|
9067
10063
|
const nPlans = listAllPlanListEntries(paths, feature).length;
|
|
9068
|
-
const hasOverview =
|
|
10064
|
+
const hasOverview = fs43.existsSync(path36.join(planDir, "00-overview.md")) ? 1 : 0;
|
|
9069
10065
|
const targets = [];
|
|
9070
|
-
if (
|
|
9071
|
-
targets.push({ label:
|
|
10066
|
+
if (fs43.existsSync(storyDir)) {
|
|
10067
|
+
targets.push({ label: path36.relative(root, storyDir), absPath: storyDir, kind: "dir" });
|
|
9072
10068
|
}
|
|
9073
|
-
if (
|
|
9074
|
-
targets.push({ label:
|
|
10069
|
+
if (fs43.existsSync(planDir)) {
|
|
10070
|
+
targets.push({ label: path36.relative(root, planDir), absPath: planDir, kind: "dir" });
|
|
9075
10071
|
}
|
|
9076
10072
|
divider("squad rm feature");
|
|
9077
10073
|
info(`Feature: ${feature}`);
|
|
@@ -9086,7 +10082,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
9086
10082
|
kv(t.kind, t.label, 6);
|
|
9087
10083
|
}
|
|
9088
10084
|
if (useTrash) {
|
|
9089
|
-
info(`Items will move to ${
|
|
10085
|
+
info(`Items will move to ${path36.relative(process.cwd(), paths.trashDir)}/<timestamp>/`);
|
|
9090
10086
|
} else if (!dryRun) {
|
|
9091
10087
|
warning("This is a permanent delete. Pass --trash to move to .squad/.trash/ instead.");
|
|
9092
10088
|
}
|
|
@@ -9119,7 +10115,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
9119
10115
|
useTrash
|
|
9120
10116
|
);
|
|
9121
10117
|
if (result.trashed) {
|
|
9122
|
-
success(`Feature removed. Items moved to ${
|
|
10118
|
+
success(`Feature removed. Items moved to ${path36.relative(root, result.trashed)}.`);
|
|
9123
10119
|
} else {
|
|
9124
10120
|
success("Feature removed (stories and plans directories).");
|
|
9125
10121
|
}
|
|
@@ -9160,11 +10156,11 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
9160
10156
|
}
|
|
9161
10157
|
}
|
|
9162
10158
|
const targets = [
|
|
9163
|
-
{ label:
|
|
10159
|
+
{ label: path37.relative(root, story.storyDir), absPath: story.storyDir, kind: "dir" }
|
|
9164
10160
|
];
|
|
9165
10161
|
if (story.planFile) {
|
|
9166
|
-
const planAbs =
|
|
9167
|
-
targets.push({ label:
|
|
10162
|
+
const planAbs = path37.join(paths.plansDir, story.feature, story.planFile);
|
|
10163
|
+
targets.push({ label: path37.relative(root, planAbs), absPath: planAbs, kind: "file" });
|
|
9168
10164
|
}
|
|
9169
10165
|
renderPreview("story", targets, {
|
|
9170
10166
|
dryRun,
|
|
@@ -9189,14 +10185,14 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
9189
10185
|
}
|
|
9190
10186
|
const result = deleteStoryRecord({ paths, story, trash: useTrash });
|
|
9191
10187
|
if (result.trashed) {
|
|
9192
|
-
success(`Removed (moved to ${
|
|
10188
|
+
success(`Removed (moved to ${path37.relative(root, result.trashed)}).`);
|
|
9193
10189
|
} else {
|
|
9194
10190
|
success("Removed story folder, plan file (if any), and overview row.");
|
|
9195
10191
|
}
|
|
9196
|
-
const featurePlanDir =
|
|
9197
|
-
const overviewPath =
|
|
9198
|
-
if (
|
|
9199
|
-
const body =
|
|
10192
|
+
const featurePlanDir = path37.join(paths.plansDir, story.feature);
|
|
10193
|
+
const overviewPath = path37.join(featurePlanDir, "00-overview.md");
|
|
10194
|
+
if (fs44.existsSync(overviewPath)) {
|
|
10195
|
+
const body = fs44.readFileSync(overviewPath, "utf8");
|
|
9200
10196
|
const onlyShell = countOverviewDataRows(body) === 0;
|
|
9201
10197
|
if (onlyShell && featureStoriesDirEmpty(paths, story.feature)) {
|
|
9202
10198
|
if (canPrompt) {
|
|
@@ -9216,7 +10212,7 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
9216
10212
|
}
|
|
9217
10213
|
|
|
9218
10214
|
// src/commands/rm/plan.ts
|
|
9219
|
-
import
|
|
10215
|
+
import path38 from "path";
|
|
9220
10216
|
import { confirm as confirm11 } from "@inquirer/prompts";
|
|
9221
10217
|
async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
9222
10218
|
const root = requireSquadRoot();
|
|
@@ -9252,7 +10248,7 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
9252
10248
|
}
|
|
9253
10249
|
const storyRows = listStories(paths, { feature: plan.feature });
|
|
9254
10250
|
const linked = storyRows.find((s) => s.planFile === plan.file);
|
|
9255
|
-
const targets = [{ label:
|
|
10251
|
+
const targets = [{ label: path38.relative(root, plan.abs), absPath: plan.abs, kind: "file" }];
|
|
9256
10252
|
renderPreview("plan", targets, {
|
|
9257
10253
|
dryRun,
|
|
9258
10254
|
trash: useTrash,
|
|
@@ -9275,8 +10271,8 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
9275
10271
|
}
|
|
9276
10272
|
}
|
|
9277
10273
|
trashOrDelete([plan.abs], paths.trashDir, useTrash);
|
|
9278
|
-
removeOverviewRowForPlanFile(
|
|
9279
|
-
const intakeHint = linked ?
|
|
10274
|
+
removeOverviewRowForPlanFile(path38.join(paths.plansDir, plan.feature), plan.file);
|
|
10275
|
+
const intakeHint = linked ? path38.relative(root, linked.intakePath) : `under .squad/stories/${plan.feature}/`;
|
|
9280
10276
|
success(`Plan removed. Run \`squad new-plan ${intakeHint}\` to regenerate.`);
|
|
9281
10277
|
}
|
|
9282
10278
|
|