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/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, path38 = []) {
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 "${[...path38, key].join(".")}" looks like a secret. Run \`squad config set planner\` or \`squad config set tracker\` to save it to .squad/secrets.yaml, then remove the key from config.yaml. Provider keys also work via ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY.`
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, [...path38, key]);
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
- switch (provider) {
841
- case "anthropic": {
842
- const res = await fetch("https://api.anthropic.com/v1/models?limit=200", {
843
- headers: { "x-api-key": apiKey, "anthropic-version": "2023-06-01" }
844
- });
845
- if (!res.ok) {
846
- return { ok: false, kind: "http", status: res.status, body: await res.text() };
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
- const body = await res.json();
849
- return { ok: true, ids: new Set((body.data ?? []).map((m) => m.id)) };
850
- }
851
- case "openai": {
852
- const res = await fetch("https://api.openai.com/v1/models", {
853
- headers: { authorization: `Bearer ${apiKey}` }
854
- });
855
- if (!res.ok) {
856
- return { ok: false, kind: "http", status: res.status, body: await res.text() };
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
- const body = await res.json();
859
- return { ok: true, ids: new Set((body.data ?? []).map((m) => m.id)) };
860
- }
861
- case "google": {
862
- const res = await fetch(
863
- `https://generativelanguage.googleapis.com/v1beta/models?pageSize=200&key=${encodeURIComponent(apiKey)}`
864
- );
865
- if (!res.ok) {
866
- return { ok: false, kind: "http", status: res.status, body: await res.text() };
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 fs31 from "fs";
3240
- import path23 from "path";
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 path19 from "path";
3333
- import fs27 from "fs";
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: randomUUID(),
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: "tool_call",
3956
+ kind: "tool_call_started",
3864
3957
  runId: opts.runId,
3865
3958
  turn,
3866
- toolCall: tc,
3867
- bytesLoaded,
3868
- totalBytes: after
3959
+ toolCallId,
3960
+ name: "read_file",
3961
+ input: tc.input
3869
3962
  });
3870
- if (readBudgetishError(result)) {
3871
- const decide = opts.decideOnLimit;
3872
- if (decide) {
3873
- const d = await decide(opts.getLimitCtx());
3874
- if (d === "cancel") {
3875
- throw new PlannerUserCancelledError(opts.getAccumulatedText(), opts.runId, turn, "budget_reads", tc, result);
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: randomUUID(),
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: "tool_call",
4039
+ kind: "tool_call_started",
3912
4040
  runId: opts.runId,
3913
4041
  turn,
3914
- toolCall: tc,
3915
- bytesLoaded,
3916
- totalBytes: after
4042
+ toolCallId,
4043
+ name: GREP_TOOL_NAME,
4044
+ input: tc.input
3917
4045
  });
3918
- return { content: out.content, isError: out.isError };
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: randomUUID(),
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: "tool_call",
4113
+ kind: "tool_call_started",
3940
4114
  runId: opts.runId,
3941
4115
  turn,
3942
- toolCall: tc,
3943
- bytesLoaded: after - before,
3944
- totalBytes: after
4116
+ toolCallId,
4117
+ name: LIST_DIR_TOOL_NAME,
4118
+ input: tc.input
3945
4119
  });
3946
- return { content: out.content, isError: out.isError };
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 fs25 from "fs/promises";
4056
- import path17 from "path";
4272
+ import fs26 from "fs/promises";
4273
+ import path18 from "path";
4057
4274
  import { randomBytes } from "crypto";
4058
- var RING_SIZE = 20;
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 appendRun(paths, rec) {
4065
- const dir = path17.join(paths.squadDir, "runs");
4066
- await fs25.mkdir(dir, { recursive: true });
4067
- const file = path17.join(dir, `${rec.runId}.json`);
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 - RING_SIZE);
4076
- await Promise.all(drop.map((n) => fs25.rm(path17.join(dir, n), { force: true })));
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 = path17.join(paths.squadDir, "runs");
4542
+ const dir = path18.join(paths.squadDir, "runs");
4080
4543
  let entries;
4081
4544
  try {
4082
- entries = await fs25.readdir(dir);
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 fs25.readFile(path17.join(dir, f), "utf8");
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 fs26 from "fs";
4875
- import path18 from "path";
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 = path18.basename(missingRel).toLowerCase();
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 = fs26.readdirSync(dir, { withFileTypes: true });
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 = path18.join(dir, ent.name);
4959
- const rel = path18.relative(root, abs).split(path18.sep).join("/");
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 = path18.basename(rel).toLowerCase();
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 = path18.resolve(input5.root, rel);
5047
- const rc = path18.relative(input5.root, resolved);
5048
- if (rc.startsWith("..") || path18.isAbsolute(rc)) return void 0;
5049
- if (!fs26.existsSync(resolved) || !fs26.statSync(resolved).isFile()) return void 0;
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 = fs26.readFileSync(resolved);
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 = path18.resolve(input5.root, c.path);
5066
- const rel = path18.relative(input5.root, resolved);
5067
- if (rel.startsWith("..") || path18.isAbsolute(rel)) continue;
5068
- if (!fs26.existsSync(resolved)) {
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 = path19.resolve(root, cleaned);
5236
- const rc = path19.relative(root, resolved);
5237
- if (rc.startsWith("..") || path19.isAbsolute(rc)) {
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 (!fs27.existsSync(resolved)) {
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 = fs27.statSync(resolved);
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 = fs27.readFileSync(resolved);
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 streamEventTextDelta(ev) {
5669
- if (!ev || typeof ev !== "object") return "";
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
- if (o.type === "content_block_delta") {
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 && d.type === "text_delta" && typeof d.text === "string") return d.text;
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
- const delta = streamEventTextDelta(m.event);
5740
- if (delta) {
5741
- text += delta;
5742
- input5.onAssistantText?.(delta);
5743
- input5.bus.emit({ kind: "assistant_text", runId: input5.runId, turn: assistantTurns, delta });
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
- if (m.type === "result") {
5848
- usage = usageFromAgentSdkResult(
5849
- m.usage
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 fs28 from "fs";
5942
- import path20 from "path";
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 = path20.join(paths.plansDir, story.feature);
5952
- fs28.mkdirSync(featurePlanDir, { recursive: true });
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 = path20.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.md`);
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
- fs28.writeFileSync(targetPath, body, "utf8");
5974
- upsertOverviewRow(featurePlanDir, story.feature, nn, path20.basename(targetPath), story, false);
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 = path20.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.partial.md`);
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
- fs28.writeFileSync(targetPath, body, "utf8");
5996
- upsertOverviewRow(featurePlanDir, story.feature, nn, path20.basename(targetPath), story, true);
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 (!fs28.existsSync(featureDir)) return void 0;
6713
+ if (!fs29.existsSync(featureDir)) return void 0;
6001
6714
  const pattern = /^(\d{2,})-story-.+\.md$/;
6002
- for (const entry of fs28.readdirSync(featureDir)) {
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: path20.join(featureDir, entry), nn: parseInt(m[1], 10) };
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 (!fs28.existsSync(featureDir)) return void 0;
6724
+ if (!fs29.existsSync(featureDir)) return void 0;
6012
6725
  const pattern = /^(\d{2,})-story-.+\.partial\.md$/;
6013
- for (const entry of fs28.readdirSync(featureDir)) {
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: path20.join(featureDir, entry), nn: parseInt(m[1], 10) };
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 = path20.join(featureDir, "00-overview.md");
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 (!fs28.existsSync(overviewPath)) {
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
- fs28.writeFileSync(overviewPath, content, "utf8");
6748
+ fs29.writeFileSync(overviewPath, content, "utf8");
6036
6749
  return;
6037
6750
  }
6038
- const current = fs28.readFileSync(overviewPath, "utf8");
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
- fs28.writeFileSync(overviewPath, lines.join("\n"), "utf8");
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 fs29 from "fs/promises";
6101
- import path21 from "path";
6813
+ import fs30 from "fs/promises";
6814
+ import path22 from "path";
6102
6815
  async function writeLastRun(paths, rec) {
6103
- const file = path21.join(paths.squadDir, ".last-run.json");
6104
- await fs29.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
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 = path21.join(paths.squadDir, ".last-run.json");
6820
+ const file = path22.join(paths.squadDir, ".last-run.json");
6108
6821
  try {
6109
- const raw = await fs29.readFile(file, "utf8");
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 fs30 from "fs";
6143
- import path22 from "path";
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 = fs30.readdirSync(dir, { withFileTypes: true });
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 = path22.join(dir, ent.name);
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 (fs30.existsSync(plansDir)) walk2(plansDir);
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 = path22.relative(paths.root, abs).split(path22.sep).join("/");
6894
+ const rel = path23.relative(paths.root, abs).split(path23.sep).join("/");
6182
6895
  let title = "";
6183
6896
  try {
6184
- const raw = fs30.readFileSync(abs, "utf8");
6185
- title = firstMarkdownH1(raw) ?? path22.basename(abs);
6897
+ const raw = fs31.readFileSync(abs, "utf8");
6898
+ title = firstMarkdownH1(raw) ?? path23.basename(abs);
6186
6899
  } catch {
6187
- title = path22.basename(abs);
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 = path23.resolve(intakePath.trim());
6226
- if (!fs31.existsSync(resolved)) {
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 path23.relative(root, path23.join(root, ".squad", "stories"));
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) => path23.resolve(s.intakePath) === path23.resolve(pick));
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
- switch (e.kind) {
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 = path23.relative(paths.root, planFile);
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
- { key: "reads", value: `${snap.reads} files \xB7 ${(snap.bytes / 1024).toFixed(1)} KB` },
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
- warning(`Validation reported ${issueCount} issue(s) (heuristic; first 10 shown):`);
6612
- for (const iss of result.validation.issues.slice(0, 10)) {
6613
- info(
6614
- ` [${iss.severity}] ${iss.kind}${iss.path ? ` ${iss.path}` : ""} \u2014 ${iss.detail}`
6615
- );
6616
- }
6617
- if (issueCount > 10) {
6618
- info(` (+${issueCount - 10} more \u2014 see metadata header issues=\u2026)`);
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 ${path23.basename(planFile)}`);
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 = path23.join(paths.squadDir, ".last-copy-prompt.md");
6643
- fs31.writeFileSync(promptFile, composed, "utf8");
6644
- const relPromptFile = path23.relative(paths.root, promptFile);
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 fs32 from "fs";
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 = fs32.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
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 path24 from "path";
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 ? path24.join(paths.plansDir, r.feature, r.planFile) : void 0;
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 fs33 from "fs";
6828
- import path25 from "path";
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 = path25.resolve(resolvedStoryPath);
6876
- const intakeFile = resolved.endsWith("intake.md") ? resolved : path25.join(resolved, "intake.md");
6877
- if (!fs33.existsSync(intakeFile)) {
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 = fs33.readFileSync(intakeFile, "utf8");
7836
+ const content = fs34.readFileSync(intakeFile, "utf8");
6883
7837
  const updated = upsertTrackerId(content, resolvedTrackerId);
6884
- fs33.writeFileSync(intakeFile, updated, "utf8");
6885
- success(`Linked tracker id ${resolvedTrackerId} \u2192 ${path25.relative(root, intakeFile)}`);
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 fs34 from "fs";
6904
- import path26 from "path";
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 = fs34.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
7867
+ ctx.secrets = fs35.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
6914
7868
  } catch (err2) {
6915
7869
  ctx.secretsError = err2;
6916
7870
  }
6917
- ctx.hasLegacyPromptsDir = fs34.existsSync(paths.promptsDir);
7871
+ ctx.hasLegacyPromptsDir = fs35.existsSync(paths.promptsDir);
6918
7872
  return ctx;
6919
7873
  }
6920
7874
  function gitignoreHasManagedBlock(repoRoot) {
6921
- const gitignore = path26.join(repoRoot, ".gitignore");
6922
- return fs34.existsSync(gitignore) && fs34.readFileSync(gitignore, "utf8").includes(".squad/secrets.yaml");
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 = path26.join(repoRoot, ".gitignore");
6926
- return fs34.existsSync(gitignore) && fs34.readFileSync(gitignore, "utf8").includes(SQUAD_TRASH_PATTERN);
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) => !fs34.existsSync(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
- fs34.mkdirSync(p, { recursive: true });
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) => path26.relative(paths.root, p)).join(", ")}`,
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 (!fs34.existsSync(paths.secretsFile)) {
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 = fs34.statSync(paths.secretsFile).mode & 511;
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
- fs34.chmodSync(paths.secretsFile, 384);
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 (!fs34.existsSync(_paths.secretsFile)) {
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 fs35 from "fs";
7582
- import path27 from "path";
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) => !fs35.existsSync(paths.promptsDir),
7588
- describe: (paths) => `delete ${path27.relative(paths.root, paths.promptsDir)} (prompts are now bundled in squad-kit)`,
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(path27.sep + ".squad" + path27.sep + "prompts")) {
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 (fs35.existsSync(target)) {
7597
- fs35.rmSync(target, { recursive: true, force: true });
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 = path27.join(paths.root, ".gitignore");
7606
- return fs35.existsSync(f) && fs35.readFileSync(f, "utf8").includes(".squad/secrets.yaml");
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 (!fs35.existsSync(paths.secretsFile)) return true;
7619
- const st = fs35.statSync(paths.secretsFile);
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 ${path27.relative(paths.root, paths.secretsFile)}`,
8576
+ describe: (paths) => `chmod 0600 ${path28.relative(paths.root, paths.secretsFile)}`,
7623
8577
  apply: (paths) => {
7624
8578
  if (process.platform === "win32") return;
7625
- if (fs35.existsSync(paths.secretsFile)) fs35.chmodSync(paths.secretsFile, 384);
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 (!fs35.existsSync(paths.configFile)) return true;
8586
+ if (!fs36.existsSync(paths.configFile)) return true;
7633
8587
  try {
7634
- const raw = fs35.readFileSync(paths.configFile, "utf8");
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 (!fs35.existsSync(paths.configFile)) return true;
8607
+ if (!fs36.existsSync(paths.configFile)) return true;
7654
8608
  try {
7655
- const raw = fs35.readFileSync(paths.configFile, "utf8");
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 (!fs35.existsSync(paths.configFile)) return;
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 fs36 from "fs";
7746
- import path28 from "path";
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 = path28.join(root, "package.json");
7758
- const pkg = JSON.parse(fs36.readFileSync(pkgFile, "utf8"));
7759
- const isDevInstall = fs36.existsSync(path28.join(root, "src"));
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 path29 from "path";
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(`${path29.sep}pnpm${path29.sep}`) || root.includes(`${path29.sep}.pnpm${path29.sep}`))
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(`${path29.sep}.yarn${path29.sep}`) || root.includes(`${path29.sep}yarn${path29.sep}`))
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(`${path29.sep}.bun${path29.sep}`) || root.includes(`${path29.sep}bun${path29.sep}`))
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 fs37 from "fs";
8881
+ import fs38 from "fs";
7928
8882
  import { z as z6 } from "zod";
7929
8883
 
7930
8884
  // src/utils/path-safety.ts
7931
- import path30 from "path";
8885
+ import path31 from "path";
7932
8886
  function isInside(abs, parent) {
7933
- const a = path30.resolve(abs) + path30.sep;
7934
- const p = path30.resolve(parent) + path30.sep;
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 = fs37.readFileSync(hit.intakePath, "utf8");
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
- fs37.writeFileSync(hit.intakePath, body.data.intakeContent, "utf8");
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 fs38 from "fs";
8036
- import path31 from "path";
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 = path31.join(opts.paths.plansDir, feature);
8052
- if (!fs38.existsSync(featureDir)) continue;
8053
- for (const entry of fs38.readdirSync(featureDir)) {
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 = path31.join(featureDir, entry);
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 = path31.resolve(path31.join(opts.paths.plansDir, feature, planFile));
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 (!fs38.existsSync(abs)) return c.json({ error: "not_found" }, 404);
8067
- const content = fs38.readFileSync(abs, "utf8");
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 = path31.join(opts.paths.plansDir, feature);
8086
- const aAbs = path31.join(featureDir, a);
8087
- const bAbs = path31.join(featureDir, b);
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 (!fs38.existsSync(aAbs) || !fs38.existsSync(bAbs)) return c.json({ error: "not_found" }, 404);
8092
- const aText = fs38.readFileSync(aAbs, "utf8");
8093
- const bText = fs38.readFileSync(bAbs, "utf8");
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 = path31.join(opts.paths.plansDir, feature, planFile);
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 (!fs38.existsSync(abs)) return c.json({ error: "not_found" }, 404);
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(path31.join(opts.paths.plansDir, feature), planFile);
9060
+ removeOverviewRowForPlanFile(path32.join(opts.paths.plansDir, feature), planFile);
8107
9061
  return c.json({ ok: true, trashed: result.trashed });
8108
9062
  }
8109
- fs38.rmSync(abs);
8110
- removeOverviewRowForPlanFile(path31.join(opts.paths.plansDir, feature), planFile);
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 path32 from "path";
8159
- import fs39 from "fs";
9112
+ import path33 from "path";
9113
+ import fs40 from "fs";
8160
9114
  var active = /* @__PURE__ */ new Map();
8161
- var EVENT_BUF_CAP = 512;
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: fs39.readFileSync(story.intakePath, "utf8")
9226
+ intakeContent: fs40.readFileSync(story.intakePath, "utf8")
8273
9227
  });
8274
9228
  let result;
8275
9229
  try {
8276
- result = await runPlanner({
8277
- root: opts.paths.root,
8278
- runtime: draftRuntime,
8279
- provider: planner.provider,
8280
- modelId,
8281
- anthropicProviderSpecific,
8282
- systemPrompt,
8283
- userPrompt,
8284
- budget,
8285
- maxOutputTokens: planner.maxOutputTokens,
8286
- cacheEnabled,
8287
- events: bus,
8288
- runId,
8289
- abort: controller.signal,
8290
- stages: {
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
- runtime: scoutRuntime,
8294
- modelId: scoutModelId,
8295
- maxFiles: maxScoutFiles,
8296
- maxOutputTokens: planner.stages?.scout?.maxOutputTokens ?? 2048
8297
- }
8298
- },
8299
- validation: { enabled: validationEnabled, strict: strictValidation },
8300
- toolsEnabled: planner.tools,
8301
- scoutSystemPrompt
8302
- });
8303
- } catch (err2) {
8304
- const durationMs = Date.now() - startedAt;
8305
- const last = eventBuffer[eventBuffer.length - 1];
8306
- const isRateLimitAbort = last?.kind === "rate_limit" && last.phase === "aborted";
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
- cacheEnabled,
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
- } catch {
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 fs40 from "fs/promises";
8702
- import path33 from "path";
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 path33.join(os.homedir(), ".squad", "recent-projects.json");
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 fs40.readFile(RECENT_FILE, "utf8");
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 fs40.mkdir(path33.dirname(RECENT_FILE), { recursive: true });
8721
- await fs40.writeFile(RECENT_FILE, JSON.stringify(list.slice(0, MAX), null, 2) + "\n", "utf8");
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 fs41 from "fs";
8751
- import path34 from "path";
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 = path34.join(distDir, "index.html");
8782
- if (!fs41.existsSync(indexHtml)) return c.text("UI not built", 404);
8783
- return c.html(fs41.readFileSync(indexHtml, "utf8"), 200, { "cache-control": "no-store" });
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 = path34.normalize(path34.join(distDir, rel));
9785
+ const abs = path35.normalize(path35.join(distDir, rel));
8790
9786
  if (!isInside2(abs, distDir)) return c.text("forbidden", 403);
8791
- if (!fs41.existsSync(abs) || !fs41.statSync(abs).isFile()) return c.text("not found", 404);
8792
- const body = Readable.toWeb(fs41.createReadStream(abs));
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 = path34.join(distDir, name);
8803
- if (!fs41.existsSync(abs)) return c.text("not found", 404);
8804
- return new Response(fs41.readFileSync(abs), {
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 = path34.resolve(abs) + path34.sep;
8820
- const p = path34.resolve(parent) + path34.sep;
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 && fs41.existsSync(path34.join(fromEnv, "index.html"))) return 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 (fs41.existsSync(path34.join(c, "index.html"))) return c;
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 fs43 from "fs";
9020
- import path36 from "path";
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 fs42 from "fs";
9025
- import path35 from "path";
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 = path35.join(storiesDir, feature);
9029
- if (!fs42.existsSync(d)) return 0;
9030
- return fs42.readdirSync(d, { withFileTypes: true }).filter((x) => x.isDirectory()).length;
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 = path35.join(paths.storiesDir, feature);
9065
- const planDir = path35.join(paths.plansDir, feature);
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 = fs42.existsSync(path35.join(planDir, "00-overview.md")) ? 1 : 0;
10064
+ const hasOverview = fs43.existsSync(path36.join(planDir, "00-overview.md")) ? 1 : 0;
9069
10065
  const targets = [];
9070
- if (fs42.existsSync(storyDir)) {
9071
- targets.push({ label: path35.relative(root, storyDir), absPath: storyDir, kind: "dir" });
10066
+ if (fs43.existsSync(storyDir)) {
10067
+ targets.push({ label: path36.relative(root, storyDir), absPath: storyDir, kind: "dir" });
9072
10068
  }
9073
- if (fs42.existsSync(planDir)) {
9074
- targets.push({ label: path35.relative(root, planDir), absPath: planDir, kind: "dir" });
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 ${path35.relative(process.cwd(), paths.trashDir)}/<timestamp>/`);
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 ${path35.relative(root, result.trashed)}.`);
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: path36.relative(root, story.storyDir), absPath: story.storyDir, kind: "dir" }
10159
+ { label: path37.relative(root, story.storyDir), absPath: story.storyDir, kind: "dir" }
9164
10160
  ];
9165
10161
  if (story.planFile) {
9166
- const planAbs = path36.join(paths.plansDir, story.feature, story.planFile);
9167
- targets.push({ label: path36.relative(root, planAbs), absPath: planAbs, kind: "file" });
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 ${path36.relative(root, result.trashed)}).`);
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 = path36.join(paths.plansDir, story.feature);
9197
- const overviewPath = path36.join(featurePlanDir, "00-overview.md");
9198
- if (fs43.existsSync(overviewPath)) {
9199
- const body = fs43.readFileSync(overviewPath, "utf8");
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 path37 from "path";
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: path37.relative(root, plan.abs), absPath: plan.abs, kind: "file" }];
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(path37.join(paths.plansDir, plan.feature), plan.file);
9279
- const intakeHint = linked ? path37.relative(root, linked.intakePath) : `under .squad/stories/${plan.feature}/`;
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