ralphctl 0.17.0 → 0.18.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.
@@ -1229,16 +1229,16 @@ var killWithEscalation = (child, graceMs = DEFAULT_KILL_GRACE_MS) => {
1229
1229
  var DEFAULT_GIT_TIMEOUT_MS = 3e4;
1230
1230
  var GIT_MAX_OUTPUT_BYTES = 50 * 1024 * 1024;
1231
1231
  var createGitRunner = (deps = {}) => {
1232
- const spawn3 = deps.spawn ?? defaultSpawn;
1232
+ const spawn2 = deps.spawn ?? defaultSpawn;
1233
1233
  const defaultTimeoutMs = deps.defaultTimeoutMs ?? DEFAULT_GIT_TIMEOUT_MS;
1234
1234
  const maxOutputBytes = deps.maxOutputBytes ?? GIT_MAX_OUTPUT_BYTES;
1235
- const run = (cwd, args, opts = {}) => runGitOnce(spawn3, cwd, args, opts.timeoutMs ?? defaultTimeoutMs, maxOutputBytes);
1235
+ const run = (cwd, args, opts = {}) => runGitOnce(spawn2, cwd, args, opts.timeoutMs ?? defaultTimeoutMs, maxOutputBytes);
1236
1236
  return { run };
1237
1237
  };
1238
- var spawnGitChild = (spawn3, cwd, args) => {
1238
+ var spawnGitChild = (spawn2, cwd, args) => {
1239
1239
  try {
1240
1240
  return Result.ok(
1241
- spawn3("git", args, {
1241
+ spawn2("git", args, {
1242
1242
  stdio: ["pipe", "pipe", "pipe"],
1243
1243
  cwd: String(cwd)
1244
1244
  })
@@ -1310,8 +1310,8 @@ var buildCloseResult = (collector, timedOut, code, timeoutMs, args, maxOutputByt
1310
1310
  );
1311
1311
  }
1312
1312
  };
1313
- var runGitOnce = (spawn3, cwd, args, timeoutMs, maxOutputBytes) => new Promise((resolve) => {
1314
- const spawnResult = spawnGitChild(spawn3, cwd, args);
1313
+ var runGitOnce = (spawn2, cwd, args, timeoutMs, maxOutputBytes) => new Promise((resolve) => {
1314
+ const spawnResult = spawnGitChild(spawn2, cwd, args);
1315
1315
  if (!spawnResult.ok) {
1316
1316
  resolve(Result.error(spawnResult.error));
1317
1317
  return;
@@ -1398,9 +1398,9 @@ var buildChildEnv = (overrideEnv) => ({
1398
1398
  ...process.env,
1399
1399
  ...overrideEnv
1400
1400
  });
1401
- var trySpawnChild = (spawn3, cwd, script, env) => {
1401
+ var trySpawnChild = (spawn2, cwd, script, env) => {
1402
1402
  try {
1403
- const child = spawn3(script, [], {
1403
+ const child = spawn2(script, [], {
1404
1404
  stdio: ["pipe", "pipe", "pipe"],
1405
1405
  cwd: String(cwd),
1406
1406
  shell: true,
@@ -1462,14 +1462,14 @@ var wireAbortLadder = (child, signal, abortKillGraceMs, isSettled2) => {
1462
1462
  };
1463
1463
  };
1464
1464
  var runChildProcess = (cwd, script, opts, deps, resolve) => {
1465
- const { spawn: spawn3, defaultTimeoutMs, now, abortKillGraceMs } = deps;
1465
+ const { spawn: spawn2, defaultTimeoutMs, now, abortKillGraceMs } = deps;
1466
1466
  const start = now();
1467
1467
  const timeoutMs = opts.timeoutMs ?? defaultTimeoutMs;
1468
1468
  if (opts.signal?.aborted === true) {
1469
1469
  resolve(abortResult());
1470
1470
  return;
1471
1471
  }
1472
- const spawnAttempt = trySpawnChild(spawn3, cwd, script, buildChildEnv(opts.env));
1472
+ const spawnAttempt = trySpawnChild(spawn2, cwd, script, buildChildEnv(opts.env));
1473
1473
  if (!spawnAttempt.ok) {
1474
1474
  resolve(Result.error(spawnAttempt.error));
1475
1475
  return;
@@ -1989,33 +1989,38 @@ var reconcileStaleProjectSiblings = async (root, id, canonicalFile) => {
1989
1989
  await removeFile(fullPath);
1990
1990
  }
1991
1991
  };
1992
- var createFsProjectRepository = (deps) => {
1993
- const list = async () => {
1994
- const dir = projectsDir(deps.root);
1995
- const entries = await listDir(dir);
1996
- if (!entries.ok) return Result.error(entries.error);
1997
- const jsonFiles = entries.value.filter((f) => f.endsWith(".json")).sort();
1998
- const items = [];
1999
- const byId = /* @__PURE__ */ new Map();
2000
- const canonicalIds = /* @__PURE__ */ new Set();
2001
- for (const file of jsonFiles) {
1992
+ var listProjects = async (root) => {
1993
+ const dir = projectsDir(root);
1994
+ const entries = await listDir(dir);
1995
+ if (!entries.ok) return Result.error(entries.error);
1996
+ const jsonFiles = entries.value.filter((f) => f.endsWith(".json")).sort();
1997
+ const reads = await Promise.all(
1998
+ jsonFiles.map(async (file) => {
2002
1999
  const path2 = `${dir}/${file}`;
2003
- const json = await readJson(path2);
2004
- if (!json.ok) {
2005
- if (json.error instanceof NotFoundError) continue;
2006
- return Result.error(json.error);
2007
- }
2008
- const decoded = decode(fromJsonProject, json.value, { entity: "project", path: path2 });
2009
- if (!decoded.ok) return Result.error(decoded.error);
2010
- const id = String(decoded.value.id);
2011
- const isCanonical = file.includes("--");
2012
- if (!isCanonical && canonicalIds.has(id)) continue;
2013
- byId.set(id, decoded.value);
2014
- if (isCanonical) canonicalIds.add(id);
2000
+ return { file, path: path2, json: await readJson(path2) };
2001
+ })
2002
+ );
2003
+ const items = [];
2004
+ const byId = /* @__PURE__ */ new Map();
2005
+ const canonicalIds = /* @__PURE__ */ new Set();
2006
+ for (const { file, path: path2, json } of reads) {
2007
+ if (!json.ok) {
2008
+ if (json.error instanceof NotFoundError) continue;
2009
+ return Result.error(json.error);
2015
2010
  }
2016
- items.push(...byId.values());
2017
- return Result.ok(items);
2018
- };
2011
+ const decoded = decode(fromJsonProject, json.value, { entity: "project", path: path2 });
2012
+ if (!decoded.ok) return Result.error(decoded.error);
2013
+ const id = String(decoded.value.id);
2014
+ const isCanonical = file.includes("--");
2015
+ if (!isCanonical && canonicalIds.has(id)) continue;
2016
+ byId.set(id, decoded.value);
2017
+ if (isCanonical) canonicalIds.add(id);
2018
+ }
2019
+ items.push(...byId.values());
2020
+ return Result.ok(items);
2021
+ };
2022
+ var createFsProjectRepository = (deps) => {
2023
+ const list = () => listProjects(deps.root);
2019
2024
  return {
2020
2025
  async findById(id) {
2021
2026
  const path2 = await resolveProjectPath(deps.root, id);
@@ -2352,33 +2357,38 @@ var reconcileSprintDir = async (root, id, canonicalDir) => {
2352
2357
  reconciledDirs.add(memoKey);
2353
2358
  }
2354
2359
  };
2355
- var createFsSprintRepository = (deps) => {
2356
- const list = async () => {
2357
- const dir = sprintsDir(deps.root);
2358
- const entries = await listDir(dir);
2359
- if (!entries.ok) return Result.error(entries.error);
2360
- const sortedEntries = [...entries.value].sort((a, b) => parseIdFromName(a).localeCompare(parseIdFromName(b)));
2361
- const items = [];
2362
- const byId = /* @__PURE__ */ new Map();
2363
- const canonicalIds = /* @__PURE__ */ new Set();
2364
- for (const entry of sortedEntries) {
2360
+ var listSprints = async (root) => {
2361
+ const dir = sprintsDir(root);
2362
+ const entries = await listDir(dir);
2363
+ if (!entries.ok) return Result.error(entries.error);
2364
+ const sortedEntries = [...entries.value].sort((a, b) => parseIdFromName(a).localeCompare(parseIdFromName(b)));
2365
+ const reads = await Promise.all(
2366
+ sortedEntries.map(async (entry) => {
2365
2367
  const path2 = `${dir}/${entry}/sprint.json`;
2366
- const json = await readJson(path2);
2367
- if (!json.ok) {
2368
- if (json.error instanceof NotFoundError) continue;
2369
- return Result.error(json.error);
2370
- }
2371
- const decoded = decode((input) => fromJsonSprint(input, path2), json.value, { entity: "sprint", path: path2 });
2372
- if (!decoded.ok) return Result.error(decoded.error);
2373
- const id = String(decoded.value.id);
2374
- const isCanonical = entry.includes("--");
2375
- if (!isCanonical && canonicalIds.has(id)) continue;
2376
- byId.set(id, decoded.value);
2377
- if (isCanonical) canonicalIds.add(id);
2368
+ return { entry, path: path2, json: await readJson(path2) };
2369
+ })
2370
+ );
2371
+ const items = [];
2372
+ const byId = /* @__PURE__ */ new Map();
2373
+ const canonicalIds = /* @__PURE__ */ new Set();
2374
+ for (const { entry, path: path2, json } of reads) {
2375
+ if (!json.ok) {
2376
+ if (json.error instanceof NotFoundError) continue;
2377
+ return Result.error(json.error);
2378
2378
  }
2379
- items.push(...byId.values());
2380
- return Result.ok(items);
2381
- };
2379
+ const decoded = decode((input) => fromJsonSprint(input, path2), json.value, { entity: "sprint", path: path2 });
2380
+ if (!decoded.ok) return Result.error(decoded.error);
2381
+ const id = String(decoded.value.id);
2382
+ const isCanonical = entry.includes("--");
2383
+ if (!isCanonical && canonicalIds.has(id)) continue;
2384
+ byId.set(id, decoded.value);
2385
+ if (isCanonical) canonicalIds.add(id);
2386
+ }
2387
+ items.push(...byId.values());
2388
+ return Result.ok(items);
2389
+ };
2390
+ var createFsSprintRepository = (deps) => {
2391
+ const list = () => listSprints(deps.root);
2382
2392
  return {
2383
2393
  async findById(id) {
2384
2394
  const dir = await resolveSprintDir(deps.root, id);
@@ -2495,6 +2505,8 @@ var unblockTask = (task) => {
2495
2505
  // The raised effort is a per-run remedy like the model bump — a clean restart drops it so the
2496
2506
  // fresh run begins on the configured effort again.
2497
2507
  escalatedToEffort: _effort,
2508
+ // Evaluator-side counterpart of the effort remedy above — same clean-restart rationale.
2509
+ escalatedToEvaluatorEffort: _evaluatorEffort,
2498
2510
  // A clean restart drops the prior run's per-criterion verdicts too — a freshly-planned task
2499
2511
  // carries no k-of-N history; stale verdicts would mislead the next run's checklist.
2500
2512
  criteriaVerdicts: _verdicts,
@@ -2505,6 +2517,7 @@ var unblockTask = (task) => {
2505
2517
  void _from;
2506
2518
  void _to;
2507
2519
  void _effort;
2520
+ void _evaluatorEffort;
2508
2521
  void _verdicts;
2509
2522
  return Result.ok({ ...rest, status: "todo", attempts: [] });
2510
2523
  };
@@ -2761,7 +2774,10 @@ var TaskBaseShape = {
2761
2774
  escalatedToModel: z13.string().optional(),
2762
2775
  // Same-model effort-rung override. Optional on read so `tasks.json` files written before the
2763
2776
  // field existed load unchanged (a missing value heals to `undefined`). Never planner-authored.
2764
- escalatedToEffort: z13.string().optional()
2777
+ escalatedToEffort: z13.string().optional(),
2778
+ // Evaluator-side counterpart of escalatedToEffort — same tolerant-read rationale. The evaluator
2779
+ // MODEL never escalates, so there is no evaluator equivalent of escalatedToModel.
2780
+ escalatedToEvaluatorEffort: z13.string().optional()
2765
2781
  };
2766
2782
  var TodoTaskSchema = z13.object({ ...TaskBaseShape, status: z13.literal("todo") });
2767
2783
  var InProgressTaskSchema = z13.object({ ...TaskBaseShape, status: z13.literal("in_progress") });
@@ -3061,6 +3077,31 @@ var isSuspendedModel = (s) => SUSPENDED_MODELS.includes(s);
3061
3077
  var SUSPENSION_NOTE = "suspended";
3062
3078
  var suspendedModelMessage = (model) => `'${model}' is temporarily suspended by its provider \u2014 pick another model until access is restored`;
3063
3079
 
3080
+ // src/integration/ai/providers/_engine/validate-model.ts
3081
+ var validateModel = (model, isKnownModel, opts) => {
3082
+ if (!isKnownModel(model)) {
3083
+ return Result.error(
3084
+ new InvalidStateError({
3085
+ entity: opts.entity,
3086
+ currentState: "model-validation",
3087
+ attemptedAction: opts.attemptedAction,
3088
+ message: opts.notKnownMessage
3089
+ })
3090
+ );
3091
+ }
3092
+ if (isSuspendedModel(model)) {
3093
+ return Result.error(
3094
+ new InvalidStateError({
3095
+ entity: opts.entity,
3096
+ currentState: "model-suspended",
3097
+ attemptedAction: opts.attemptedAction,
3098
+ message: suspendedModelMessage(model)
3099
+ })
3100
+ );
3101
+ }
3102
+ return Result.ok(void 0);
3103
+ };
3104
+
3064
3105
  // src/integration/ai/providers/_engine/bounded-tail.ts
3065
3106
  var STDERR_TAIL_CAP = 16384;
3066
3107
  var RATE_LIMIT_SCAN_TAIL_CAP = 8192;
@@ -3211,7 +3252,7 @@ var defaultProviderSpawn = (command, args, options) => crossPlatformSpawn(comman
3211
3252
  // src/integration/ai/providers/_engine/truncate-debug-field.ts
3212
3253
  var truncateField = (value, max = 120) => {
3213
3254
  if (value === void 0 || value === null) return void 0;
3214
- if (value.length === 0) return void 0;
3255
+ if (value.trim().length === 0) return void 0;
3215
3256
  if (value.length <= max) return value;
3216
3257
  return `${value.slice(0, max)}\u2026`;
3217
3258
  };
@@ -3360,6 +3401,12 @@ var delayForRetry = (retryIndex, schedule = DEFAULT_BACKOFF_SCHEDULE) => {
3360
3401
  const clamped = Math.min(retryIndex - 1, schedule.length - 1);
3361
3402
  return schedule[clamped] ?? 0;
3362
3403
  };
3404
+ var JITTER_RATIO = 0.2;
3405
+ var applyJitter = (delayMs, random = Math.random) => {
3406
+ if (delayMs <= 0) return delayMs;
3407
+ const spread = delayMs * JITTER_RATIO;
3408
+ return Math.round(delayMs - spread + random() * spread * 2);
3409
+ };
3363
3410
  var sleepCancellable = (ms, signal) => new Promise((resolve) => {
3364
3411
  if (signal?.aborted === true) {
3365
3412
  resolve();
@@ -3394,7 +3441,8 @@ var handleRateLimitOutcome = async ({
3394
3441
  attemptIdx,
3395
3442
  maxAttempts,
3396
3443
  schedule,
3397
- error
3444
+ error,
3445
+ random
3398
3446
  }) => {
3399
3447
  const bannerId = `rate-limit-${providerSlug}-${error.sessionId ?? String(attemptIdx + 1)}`;
3400
3448
  eventBus.publish({
@@ -3408,7 +3456,7 @@ var handleRateLimitOutcome = async ({
3408
3456
  if (attemptIdx >= maxAttempts - 1) {
3409
3457
  return { type: "continue", session: nextSession };
3410
3458
  }
3411
- const delayMs = delayForRetry(attemptIdx + 1, schedule);
3459
+ const delayMs = applyJitter(delayForRetry(attemptIdx + 1, schedule), random ?? Math.random);
3412
3460
  if (delayMs <= 0) {
3413
3461
  return { type: "continue", session: nextSession };
3414
3462
  }
@@ -3472,7 +3520,8 @@ var runWithRateLimitRetry = async (opts) => {
3472
3520
  attemptIdx,
3473
3521
  maxAttempts,
3474
3522
  schedule,
3475
- error: outcome.error
3523
+ error: outcome.error,
3524
+ random: opts.random
3476
3525
  });
3477
3526
  if (result.type === "aborted") {
3478
3527
  return Result.error(result.error);
@@ -3918,26 +3967,12 @@ var disallowedToolsFor = (p) => {
3918
3967
  return denied;
3919
3968
  };
3920
3969
  var buildClaudeArgs = (session) => {
3921
- if (!isClaudeModel(session.model)) {
3922
- return Result.error(
3923
- new InvalidStateError({
3924
- entity: PROVIDER_NAME,
3925
- currentState: "model-validation",
3926
- attemptedAction: "build argv",
3927
- message: `claude-provider: '${session.model}' is not a known Claude model`
3928
- })
3929
- );
3930
- }
3931
- if (isSuspendedModel(session.model)) {
3932
- return Result.error(
3933
- new InvalidStateError({
3934
- entity: PROVIDER_NAME,
3935
- currentState: "model-suspended",
3936
- attemptedAction: "build argv",
3937
- message: suspendedModelMessage(session.model)
3938
- })
3939
- );
3940
- }
3970
+ const validated = validateModel(session.model, isClaudeModel, {
3971
+ entity: PROVIDER_NAME,
3972
+ attemptedAction: "build argv",
3973
+ notKnownMessage: `claude-provider: '${session.model}' is not a known Claude model`
3974
+ });
3975
+ if (!validated.ok) return Result.error(validated.error);
3941
3976
  const args = ["-p", "--verbose", "--output-format", "stream-json", "--model", session.model];
3942
3977
  args.push("--permission-mode", "bypassPermissions");
3943
3978
  const denied = disallowedToolsFor(session.permissions);
@@ -4039,16 +4074,12 @@ var sandboxFor = (_p) => {
4039
4074
  return Result.ok({ sandbox: "workspace-write" });
4040
4075
  };
4041
4076
  var buildCodexArgs = (session, opts) => {
4042
- if (!isCodexModel(session.model)) {
4043
- return Result.error(
4044
- new InvalidStateError({
4045
- entity: PROVIDER_NAME2,
4046
- currentState: "model-validation",
4047
- attemptedAction: "build argv",
4048
- message: `codex-provider: '${session.model}' is not a known Codex model`
4049
- })
4050
- );
4051
- }
4077
+ const validated = validateModel(session.model, isCodexModel, {
4078
+ entity: PROVIDER_NAME2,
4079
+ attemptedAction: "build argv",
4080
+ notKnownMessage: `codex-provider: '${session.model}' is not a known Codex model`
4081
+ });
4082
+ if (!validated.ok) return Result.error(validated.error);
4052
4083
  const perms = sandboxFor(session.permissions);
4053
4084
  if (!perms.ok) return Result.error(perms.error);
4054
4085
  const args = ["exec"];
@@ -4409,26 +4440,12 @@ var RESUME_STALE_RE3 = /(session|conversation)[^\n]*not found|no (session|conver
4409
4440
  var isFullAuto = (p) => p.autoApprove && p.canModifyRepoFiles && p.canRunShell;
4410
4441
  var PROVIDER_NAME3 = "copilot-provider";
4411
4442
  var buildCopilotArgs = (session) => {
4412
- if (!isCopilotModel(session.model)) {
4413
- return Result.error(
4414
- new InvalidStateError({
4415
- entity: PROVIDER_NAME3,
4416
- currentState: "model-validation",
4417
- attemptedAction: "build argv",
4418
- message: `copilot-provider: '${session.model}' is not a known Copilot model`
4419
- })
4420
- );
4421
- }
4422
- if (isSuspendedModel(session.model)) {
4423
- return Result.error(
4424
- new InvalidStateError({
4425
- entity: PROVIDER_NAME3,
4426
- currentState: "model-suspended",
4427
- attemptedAction: "build argv",
4428
- message: suspendedModelMessage(session.model)
4429
- })
4430
- );
4431
- }
4443
+ const validated = validateModel(session.model, isCopilotModel, {
4444
+ entity: PROVIDER_NAME3,
4445
+ attemptedAction: "build argv",
4446
+ notKnownMessage: `copilot-provider: '${session.model}' is not a known Copilot model`
4447
+ });
4448
+ if (!validated.ok) return Result.error(validated.error);
4432
4449
  const args = [
4433
4450
  "--output-format=json",
4434
4451
  "--autopilot",
@@ -4657,16 +4674,12 @@ var createInteractiveClaudeProvider = (deps) => {
4657
4674
  const newSessionId = deps.newSessionId ?? uuidv7;
4658
4675
  return {
4659
4676
  async run(input) {
4660
- if (!isClaudeModel(input.model)) {
4661
- return Result.error(
4662
- new InvalidStateError({
4663
- entity: PROVIDER,
4664
- currentState: "model-validation",
4665
- attemptedAction: "run",
4666
- message: `interactive-claude: '${input.model}' is not a known Claude model`
4667
- })
4668
- );
4669
- }
4677
+ const validated = validateModel(input.model, isClaudeModel, {
4678
+ entity: PROVIDER,
4679
+ attemptedAction: "run",
4680
+ notKnownMessage: `interactive-claude: '${input.model}' is not a known Claude model`
4681
+ });
4682
+ if (!validated.ok) return Result.error(validated.error);
4670
4683
  let prompt;
4671
4684
  try {
4672
4685
  prompt = await readFile5(String(input.promptFile));
@@ -4777,16 +4790,12 @@ var createInteractiveCodexProvider = (deps) => {
4777
4790
  const readFile5 = deps.readFile ?? defaultReadFile;
4778
4791
  return {
4779
4792
  async run(input) {
4780
- if (!isCodexModel(input.model)) {
4781
- return Result.error(
4782
- new InvalidStateError({
4783
- entity: PROVIDER2,
4784
- currentState: "model-validation",
4785
- attemptedAction: "run",
4786
- message: `interactive-codex: '${input.model}' is not a known Codex model`
4787
- })
4788
- );
4789
- }
4793
+ const validated = validateModel(input.model, isCodexModel, {
4794
+ entity: PROVIDER2,
4795
+ attemptedAction: "run",
4796
+ notKnownMessage: `interactive-codex: '${input.model}' is not a known Codex model`
4797
+ });
4798
+ if (!validated.ok) return Result.error(validated.error);
4790
4799
  let prompt;
4791
4800
  try {
4792
4801
  prompt = await readFile5(String(input.promptFile));
@@ -4887,16 +4896,12 @@ var createInteractiveCopilotProvider = (deps) => {
4887
4896
  const newSessionId = deps.newSessionId ?? uuidv7;
4888
4897
  return {
4889
4898
  async run(input) {
4890
- if (!isCopilotModel(input.model)) {
4891
- return Result.error(
4892
- new InvalidStateError({
4893
- entity: PROVIDER3,
4894
- currentState: "model-validation",
4895
- attemptedAction: "run",
4896
- message: `interactive-copilot: '${input.model}' is not a known Copilot model`
4897
- })
4898
- );
4899
- }
4899
+ const validated = validateModel(input.model, isCopilotModel, {
4900
+ entity: PROVIDER3,
4901
+ attemptedAction: "run",
4902
+ notKnownMessage: `interactive-copilot: '${input.model}' is not a known Copilot model`
4903
+ });
4904
+ if (!validated.ok) return Result.error(validated.error);
4900
4905
  let prompt;
4901
4906
  try {
4902
4907
  prompt = await readFile5(String(input.promptFile));
@@ -5016,10 +5021,10 @@ var createInteractiveAiProvider = (deps) => {
5016
5021
  };
5017
5022
 
5018
5023
  // src/integration/io/run-cli.ts
5019
- var runCli = (spawn3, command, args, opts) => new Promise((resolve) => {
5024
+ var runCli = (spawn2, command, args, opts) => new Promise((resolve) => {
5020
5025
  let child;
5021
5026
  try {
5022
- child = spawn3(command, args, {
5027
+ child = spawn2(command, args, {
5023
5028
  stdio: ["pipe", "pipe", "pipe"],
5024
5029
  ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {}
5025
5030
  });
@@ -5133,9 +5138,9 @@ var looksLikeNotFound = (stderr) => {
5133
5138
  const s = stderr.toLowerCase();
5134
5139
  return s.includes("not found") || s.includes("404") || s.includes("could not resolve") || s.includes("does not exist");
5135
5140
  };
5136
- var fetchGitHub = async (spawn3, parsed, url) => {
5141
+ var fetchGitHub = async (spawn2, parsed, url) => {
5137
5142
  const result = await runCli(
5138
- spawn3,
5143
+ spawn2,
5139
5144
  "gh",
5140
5145
  [
5141
5146
  "issue",
@@ -5182,9 +5187,9 @@ var fetchGitHub = async (spawn3, parsed, url) => {
5182
5187
  comments
5183
5188
  });
5184
5189
  };
5185
- var fetchGitLabNotes = async (spawn3, parsed) => {
5190
+ var fetchGitLabNotes = async (spawn2, parsed) => {
5186
5191
  const result = await runCli(
5187
- spawn3,
5192
+ spawn2,
5188
5193
  "glab",
5189
5194
  ["issue", "note", "list", String(parsed.number), "--repo", glabRepoArg(parsed), "--output", "json"],
5190
5195
  { timeoutMs: CLI_TIMEOUT_MS }
@@ -5213,9 +5218,9 @@ var fetchGitLabNotes = async (spawn3, parsed) => {
5213
5218
  }));
5214
5219
  return { comments };
5215
5220
  };
5216
- var fetchGitLab = async (spawn3, parsed, url, logger) => {
5221
+ var fetchGitLab = async (spawn2, parsed, url, logger) => {
5217
5222
  const result = await runCli(
5218
- spawn3,
5223
+ spawn2,
5219
5224
  "glab",
5220
5225
  ["issue", "view", String(parsed.number), "--repo", glabRepoArg(parsed), "--output", "json"],
5221
5226
  { timeoutMs: CLI_TIMEOUT_MS }
@@ -5242,7 +5247,7 @@ var fetchGitLab = async (spawn3, parsed, url, logger) => {
5242
5247
  })
5243
5248
  );
5244
5249
  }
5245
- const notes = await fetchGitLabNotes(spawn3, parsed);
5250
+ const notes = await fetchGitLabNotes(spawn2, parsed);
5246
5251
  if (notes.failure !== void 0) {
5247
5252
  logger?.warn(`glab issue note list failed for ${url}: ${notes.failure} \u2014 proceeding without comments`);
5248
5253
  }
@@ -5263,9 +5268,9 @@ var createIssueFetcher = (deps) => async (url) => {
5263
5268
 
5264
5269
  // src/integration/scm/issue-pusher.ts
5265
5270
  var CLI_TIMEOUT_MS2 = 3e4;
5266
- var commentGitHub = async (spawn3, url, body, parsed) => {
5271
+ var commentGitHub = async (spawn2, url, body, parsed) => {
5267
5272
  const r = await runCli(
5268
- spawn3,
5273
+ spawn2,
5269
5274
  "gh",
5270
5275
  ["issue", "comment", String(parsed.number), "--repo", `${parsed.owner}/${parsed.repo}`, "--body-file", "-"],
5271
5276
  { stdin: body, timeoutMs: CLI_TIMEOUT_MS2 }
@@ -5281,9 +5286,9 @@ var commentGitHub = async (spawn3, url, body, parsed) => {
5281
5286
  }
5282
5287
  return Result.ok(void 0);
5283
5288
  };
5284
- var commentGitLab = async (spawn3, url, body, parsed) => {
5289
+ var commentGitLab = async (spawn2, url, body, parsed) => {
5285
5290
  const r = await runCli(
5286
- spawn3,
5291
+ spawn2,
5287
5292
  "glab",
5288
5293
  [
5289
5294
  "issue",
@@ -5408,11 +5413,11 @@ var buildGlabArgs = (input) => {
5408
5413
  if (input.draft) args.push("--draft");
5409
5414
  return args;
5410
5415
  };
5411
- var runPlatformCli = async (spawn3, platform, input) => {
5416
+ var runPlatformCli = async (spawn2, platform, input) => {
5412
5417
  const command = platform === "github" ? "gh" : "glab";
5413
5418
  const args = platform === "github" ? buildGhArgs(input) : buildGlabArgs(input);
5414
5419
  const noun = platform === "github" ? "gh pr create" : "glab mr create";
5415
- const result = await runCli(spawn3, command, args, { cwd: String(input.cwd), timeoutMs: CLI_TIMEOUT_MS3 });
5420
+ const result = await runCli(spawn2, command, args, { cwd: String(input.cwd), timeoutMs: CLI_TIMEOUT_MS3 });
5416
5421
  if (!result.ok) return Result.error(result.error);
5417
5422
  if (result.value.exitCode !== 0) {
5418
5423
  const stderr = result.value.stderr.trim();
@@ -6024,7 +6029,7 @@ var createNpmVersionChecker = (deps) => {
6024
6029
  // package.json
6025
6030
  var package_default = {
6026
6031
  name: "ralphctl",
6027
- version: "0.17.0",
6032
+ version: "0.18.0",
6028
6033
  description: "Agent harness for long-running AI coding tasks \u2014 orchestrates Claude Code, GitHub Copilot, and OpenAI Codex across repositories",
6029
6034
  homepage: "https://github.com/lukas-grigis/ralphctl",
6030
6035
  type: "module",
@@ -6526,18 +6531,7 @@ var BUNDLED_SKILLS = [
6526
6531
  ];
6527
6532
  var skillsForFlow = (flowId) => BUNDLED_SKILLS.filter((entry) => entry.defaultFor.includes(flowId)).map((entry) => entry.name);
6528
6533
 
6529
- // src/integration/ai/skills/_engine/skill.ts
6530
- import { z as z16 } from "zod";
6531
- var SkillNameSchema = z16.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u, "lowercase alphanumeric with hyphens, no leading/trailing/consecutive hyphens");
6532
- var SkillFrontmatterSchema = z16.object({
6533
- name: SkillNameSchema,
6534
- description: z16.string().min(1).max(1024),
6535
- license: z16.string().optional(),
6536
- compatibility: z16.string().max(500).optional(),
6537
- "allowed-tools": z16.string().optional()
6538
- });
6539
-
6540
- // src/integration/ai/skills/_engine/parse-skill.ts
6534
+ // src/integration/ai/skills/_engine/frontmatter.ts
6541
6535
  var splitFrontmatter = (raw) => {
6542
6536
  const trimmed = raw.replace(/^\uFEFF/u, "");
6543
6537
  if (!trimmed.startsWith("---")) return { frontmatter: "", body: trimmed };
@@ -6564,6 +6558,19 @@ var parseSimpleYaml = (input) => {
6564
6558
  return result;
6565
6559
  };
6566
6560
  var errorCode = (cause) => typeof cause === "object" && cause !== null && "code" in cause && typeof cause.code === "string" ? cause.code : void 0;
6561
+
6562
+ // src/integration/ai/skills/_engine/skill.ts
6563
+ import { z as z16 } from "zod";
6564
+ var SkillNameSchema = z16.string().min(1).max(64).regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u, "lowercase alphanumeric with hyphens, no leading/trailing/consecutive hyphens");
6565
+ var SkillFrontmatterSchema = z16.object({
6566
+ name: SkillNameSchema,
6567
+ description: z16.string().min(1).max(1024),
6568
+ license: z16.string().optional(),
6569
+ compatibility: z16.string().max(500).optional(),
6570
+ "allowed-tools": z16.string().optional()
6571
+ });
6572
+
6573
+ // src/integration/ai/skills/_engine/parse-skill.ts
6567
6574
  var parseSkill = (label, path2, name, raw) => {
6568
6575
  const { frontmatter, body } = splitFrontmatter(raw);
6569
6576
  const fm = parseSimpleYaml(frontmatter);
@@ -7293,35 +7300,9 @@ import { readFile as readFile4 } from "fs/promises";
7293
7300
  var BUNDLED_AGENT_DEFINITIONS = ["ralphctl-evaluator", "ralphctl-generator"];
7294
7301
 
7295
7302
  // src/integration/ai/agents/_engine/parse-agent-definition.ts
7296
- var splitFrontmatter2 = (raw) => {
7297
- const trimmed = raw.replace(/^\uFEFF/u, "");
7298
- if (!trimmed.startsWith("---")) return { frontmatter: "", body: trimmed };
7299
- const closing = trimmed.indexOf("\n---", 3);
7300
- if (closing === -1) return { frontmatter: "", body: trimmed };
7301
- const frontmatter = trimmed.slice(3, closing).trim();
7302
- const afterClose = trimmed.slice(closing + 4);
7303
- const body = afterClose.replace(/^(?:\r?\n)+/, "");
7304
- return { frontmatter, body };
7305
- };
7306
- var parseSimpleYaml2 = (input) => {
7307
- const result = {};
7308
- for (const line of input.split("\n")) {
7309
- const stripped = line.trim();
7310
- if (stripped.length === 0 || stripped.startsWith("#")) continue;
7311
- const colon = stripped.indexOf(":");
7312
- if (colon === -1) continue;
7313
- const key = stripped.slice(0, colon).trim();
7314
- let value = stripped.slice(colon + 1).trim();
7315
- if (value.startsWith('"') && value.endsWith('"')) value = value.slice(1, -1);
7316
- else if (value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
7317
- result[key] = value;
7318
- }
7319
- return result;
7320
- };
7321
- var errorCode2 = (cause) => typeof cause === "object" && cause !== null && "code" in cause && typeof cause.code === "string" ? cause.code : void 0;
7322
7303
  var parseAgentDefinition = (label, path2, name, raw) => {
7323
- const { frontmatter, body } = splitFrontmatter2(raw);
7324
- const fm = parseSimpleYaml2(frontmatter);
7304
+ const { frontmatter, body } = splitFrontmatter(raw);
7305
+ const fm = parseSimpleYaml(frontmatter);
7325
7306
  const parsed = AgentDefinitionFrontmatterSchema.safeParse(fm);
7326
7307
  if (!parsed.success) {
7327
7308
  return Result.error(
@@ -7375,7 +7356,7 @@ var readDefinitionOptional = async (root, name) => {
7375
7356
  try {
7376
7357
  raw = await readFile4(path2, "utf-8");
7377
7358
  } catch (cause) {
7378
- if (errorCode2(cause) === "ENOENT") return Result.ok(void 0);
7359
+ if (errorCode(cause) === "ENOENT") return Result.ok(void 0);
7379
7360
  return Result.error(
7380
7361
  new StorageError({ subCode: "io", message: `bundled agent definition not readable: ${path2}`, path: path2, cause })
7381
7362
  );
@@ -7423,7 +7404,7 @@ var loadOperatorAgentDefinitions = async (deps) => {
7423
7404
  try {
7424
7405
  entries = await fs14.readdir(root, { withFileTypes: true });
7425
7406
  } catch (cause) {
7426
- if (errorCode2(cause) === "ENOENT") return [];
7407
+ if (errorCode(cause) === "ENOENT") return [];
7427
7408
  log.warn("operator agent definitions dir not readable", { path: root, cause });
7428
7409
  return [];
7429
7410
  }
@@ -7656,7 +7637,7 @@ var buildWireAgentDefinitionSource = (storage2, logger) => composeAgentDefinitio
7656
7637
  })
7657
7638
  );
7658
7639
  var wire = (opts) => {
7659
- const spawn3 = opts.spawn ?? defaultPipeSpawn;
7640
+ const spawn2 = opts.spawn ?? defaultPipeSpawn;
7660
7641
  const env = opts.env ?? process.env;
7661
7642
  const debugTrace = isTruthyEnvFlag(env[RALPHCTL_DEBUG_TRACE_ENV]);
7662
7643
  const appendFile = createAppendFile();
@@ -7716,9 +7697,9 @@ var wire = (opts) => {
7716
7697
  availableModelsFor,
7717
7698
  eventBus,
7718
7699
  logger,
7719
- pullRequestCreator: createPullRequestCreator({ gitRunner: createGitRunner(), spawn: spawn3 }),
7720
- issueFetcher: createIssueFetcher({ spawn: spawn3, logger }),
7721
- issuePusher: createIssuePusher({ spawn: spawn3 }),
7700
+ pullRequestCreator: createPullRequestCreator({ gitRunner: createGitRunner(), spawn: spawn2 }),
7701
+ issueFetcher: createIssueFetcher({ spawn: spawn2, logger }),
7702
+ issuePusher: createIssuePusher({ spawn: spawn2 }),
7722
7703
  versionChecker: createNpmVersionChecker({
7723
7704
  stateRoot: opts.storage.stateRoot,
7724
7705
  currentVersion: CLI_METADATA.currentVersion,
@@ -8954,9 +8935,8 @@ var useSelection = () => {
8954
8935
  import { createContext as createContext10, useCallback as useCallback5, useContext as useContext10, useEffect as useEffect4, useState as useState6 } from "react";
8955
8936
 
8956
8937
  // src/integration/io/command-exists.ts
8957
- import { spawn as spawn2 } from "child_process";
8958
8938
  var commandExists = (name) => new Promise((resolve) => {
8959
- const child = process.platform === "win32" ? spawn2("where", [name], { stdio: "ignore" }) : spawn2("sh", ["-c", 'command -v "$0"', name], { stdio: "ignore" });
8939
+ const child = process.platform === "win32" ? crossPlatformSpawn("where", [name], { stdio: "ignore" }) : crossPlatformSpawn("sh", ["-c", 'command -v "$0"', name], { stdio: "ignore" });
8960
8940
  let settled = false;
8961
8941
  const settle = (value) => {
8962
8942
  if (settled) return;
@@ -10033,7 +10013,7 @@ var Breadcrumb = () => {
10033
10013
  /* @__PURE__ */ jsx14(Text5, { color: inkColors.primary, bold: true, children: right[1] }),
10034
10014
  /* @__PURE__ */ jsx14(Text5, { dimColor: true, children: " " }),
10035
10015
  /* @__PURE__ */ jsx14(Text5, { color: inkColors.highlight, bold: true, children: "[S]" }),
10036
- atLeast("md") && effectiveSprintStatus !== void 0 && /* @__PURE__ */ jsx14(Box3, { marginLeft: 1, children: /* @__PURE__ */ jsx14(StatusChip, { label: effectiveSprintStatus, kind: sprintStatusKind(effectiveSprintStatus) }) })
10016
+ atLeast("md") && effectiveSprintStatus !== void 0 && /* @__PURE__ */ jsx14(Box3, { marginLeft: spacing.gutter, children: /* @__PURE__ */ jsx14(StatusChip, { label: effectiveSprintStatus, kind: sprintStatusKind(effectiveSprintStatus) }) })
10037
10017
  ] })
10038
10018
  ] })
10039
10019
  ]
@@ -11104,7 +11084,7 @@ var ScrollableMessage = ({ message, ownsArrows = true }) => {
11104
11084
  " ",
11105
11085
  header
11106
11086
  ] }),
11107
- body.length > 0 && /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", marginTop: 1, children: [
11087
+ body.length > 0 && /* @__PURE__ */ jsxs12(Box11, { flexDirection: "column", marginTop: spacing.section, children: [
11108
11088
  /* @__PURE__ */ jsx22(Box11, { flexDirection: "column", borderStyle: "single", borderColor: inkColors.rule, paddingX: spacing.cardPadX, children: visible.map((line, i) => /* @__PURE__ */ jsx22(Text13, { children: line.length === 0 ? " " : line }, `body-${String(offset + i)}`)) }),
11109
11089
  overflows && /* @__PURE__ */ jsxs12(Text13, { dimColor: true, children: [
11110
11090
  "lines ",
@@ -11146,7 +11126,7 @@ var ConfirmPrompt = ({
11146
11126
  const Pill = ({ on, label }) => /* @__PURE__ */ jsx23(Text14, { color: on ? inkColors.primary : inkColors.muted, bold: on, children: on ? `[ ${label} ]` : ` ${label} ` });
11147
11127
  return /* @__PURE__ */ jsxs13(Box12, { flexDirection: "column", paddingX: spacing.indent, children: [
11148
11128
  /* @__PURE__ */ jsx23(ScrollableMessage, { message }),
11149
- /* @__PURE__ */ jsxs13(Box12, { marginTop: 1, children: [
11129
+ /* @__PURE__ */ jsxs13(Box12, { marginTop: spacing.section, children: [
11150
11130
  /* @__PURE__ */ jsx23(Pill, { on: yes, label: "Yes" }),
11151
11131
  /* @__PURE__ */ jsx23(Text14, { children: " " }),
11152
11132
  /* @__PURE__ */ jsx23(Pill, { on: !yes, label: "No" })
@@ -11209,7 +11189,7 @@ var SelectPrompt = ({
11209
11189
  const end = Math.min(options.length, start + VISIBLE_ROWS);
11210
11190
  return /* @__PURE__ */ jsxs14(Box13, { flexDirection: "column", paddingX: spacing.indent, children: [
11211
11191
  /* @__PURE__ */ jsx24(ScrollableMessage, { message, ownsArrows: false }),
11212
- /* @__PURE__ */ jsx24(Box13, { flexDirection: "column", marginTop: 1, children: options.slice(start, end).map((opt, localIdx) => {
11192
+ /* @__PURE__ */ jsx24(Box13, { flexDirection: "column", marginTop: spacing.section, children: options.slice(start, end).map((opt, localIdx) => {
11213
11193
  const i = start + localIdx;
11214
11194
  const focused = i === cursor;
11215
11195
  const disabled = opt.disabled === true;
@@ -11352,7 +11332,7 @@ var MultiSelectPrompt = ({
11352
11332
  " ",
11353
11333
  message
11354
11334
  ] }),
11355
- /* @__PURE__ */ jsx25(Box14, { flexDirection: "column", marginTop: 1, children: options.slice(start, end).map((opt, localIdx) => {
11335
+ /* @__PURE__ */ jsx25(Box14, { flexDirection: "column", marginTop: spacing.section, children: options.slice(start, end).map((opt, localIdx) => {
11356
11336
  const i = start + localIdx;
11357
11337
  const focused = i === cursor && opt.disabled !== true;
11358
11338
  return /* @__PURE__ */ jsx25(OptionRow, { opt, focused, checked: picked.has(i) }, `opt-${String(i)}`);
@@ -11585,6 +11565,9 @@ var OverflowRow = ({ direction, count }) => {
11585
11565
  // src/application/ui/tui/components/action-menu.tsx
11586
11566
  import { jsx as jsx29, jsxs as jsxs19 } from "react/jsx-runtime";
11587
11567
  var isEnabled3 = (item) => item.disabledReason === void 0;
11568
+ var RESERVED_NAV_KEYS = new Set(
11569
+ [...listKeys.up.keys, ...listKeys.down.keys].filter((k) => /^[a-z]$/.test(k))
11570
+ );
11588
11571
  var findInitialCursorId = (items, initialIndex, enabledItems) => {
11589
11572
  for (let i = initialIndex; i < items.length; i++) {
11590
11573
  const it = items[i];
@@ -11592,7 +11575,10 @@ var findInitialCursorId = (items, initialIndex, enabledItems) => {
11592
11575
  }
11593
11576
  return enabledItems[0]?.id ?? "";
11594
11577
  };
11595
- var matchHotkey = (items, input) => items.find((it) => it.hotkey === input && it.globalHotkey !== true && isEnabled3(it));
11578
+ var matchHotkey = (items, input) => {
11579
+ if (RESERVED_NAV_KEYS.has(input)) return void 0;
11580
+ return items.find((it) => it.hotkey === input && it.globalHotkey !== true && isEnabled3(it));
11581
+ };
11596
11582
  var isRowVisible = (it, enabled, visibleEnabledIds, windowedEnabled) => {
11597
11583
  if (enabled) return visibleEnabledIds.has(it.id);
11598
11584
  if (windowedEnabled.length === 0) return true;
@@ -14001,14 +13987,20 @@ var warnIfContractViolated = (logger, skillName, content) => {
14001
13987
  };
14002
13988
 
14003
13989
  // src/business/settings/resolve-effort.ts
13990
+ var FLOW_DEFAULT_EFFORT = {
13991
+ plan: "high",
13992
+ ideate: "high"
13993
+ };
14004
13994
  var resolveEffort = (flow, settings) => {
14005
13995
  const row = primaryFlowRow(settings.ai, flow);
14006
- return resolveEffortForRow(row, settings.ai.effort);
13996
+ return resolveEffortForRow(row, settings.ai.effort, flow);
14007
13997
  };
14008
- var resolveEffortForRow = (row, globalEffort) => {
13998
+ var resolveEffortForRow = (row, globalEffort, flow) => {
14009
13999
  if (row.effort !== void 0) return row.effort;
14010
- if (globalEffort === void 0) return void 0;
14011
- return _floorForProvider(globalEffort, row.provider);
14000
+ if (globalEffort !== void 0) return _floorForProvider(globalEffort, row.provider);
14001
+ const flowDefault = flow !== void 0 ? FLOW_DEFAULT_EFFORT[flow] : void 0;
14002
+ if (flowDefault !== void 0) return clampEffortToProvider(flowDefault, row.provider);
14003
+ return void 0;
14012
14004
  };
14013
14005
  var clampEffortToProvider = (effort, provider) => {
14014
14006
  if (provider === "openai-codex" && effort === "max") return "xhigh";
@@ -14585,6 +14577,28 @@ var interactiveSprintNameLeaf = (deps) => leaf("interactive-sprint-name", {
14585
14577
  // src/application/flows/create-sprint/leaves/init-progress-journal.ts
14586
14578
  import { join as join31 } from "path";
14587
14579
 
14580
+ // src/business/_shared/format-duration.ts
14581
+ var EM_DASH = "\u2014";
14582
+ var MS_PER_SECOND = 1e3;
14583
+ var MS_PER_MINUTE = 60 * MS_PER_SECOND;
14584
+ var MS_PER_HOUR = 60 * MS_PER_MINUTE;
14585
+ var formatDuration = (ms) => {
14586
+ if (ms === void 0) return EM_DASH;
14587
+ if (ms < 0) return `${String(ms)}ms`;
14588
+ if (ms < MS_PER_SECOND) return `${String(ms)}ms`;
14589
+ if (ms < MS_PER_MINUTE) {
14590
+ return `${String(Math.floor(ms / MS_PER_SECOND))}s`;
14591
+ }
14592
+ if (ms < MS_PER_HOUR) {
14593
+ const minutes2 = Math.floor(ms / MS_PER_MINUTE);
14594
+ const seconds = Math.floor(ms % MS_PER_MINUTE / MS_PER_SECOND);
14595
+ return seconds > 0 ? `${String(minutes2)}m ${String(seconds)}s` : `${String(minutes2)}m`;
14596
+ }
14597
+ const hours = Math.floor(ms / MS_PER_HOUR);
14598
+ const minutes = Math.floor(ms % MS_PER_HOUR / MS_PER_MINUTE);
14599
+ return minutes > 0 ? `${String(hours)}h ${String(minutes)}m` : `${String(hours)}h`;
14600
+ };
14601
+
14588
14602
  // src/business/sprint/journal-sanitize.ts
14589
14603
  var sanitizeInline = (text) => text.replace(/[\r\n\t\v\f]+/g, " ").trim();
14590
14604
  var neutralizeProseHeadings = (text) => text.split("\n").map((line) => line.startsWith("#") ? ` ${line}` : line).join("\n");
@@ -14628,26 +14642,7 @@ _Task ${sanitizeInline(taskName)}: rejected diff quarantined to git stash \u2014
14628
14642
  `;
14629
14643
 
14630
14644
  // src/business/sprint/render-journal-entry.ts
14631
- var EM_DASH = "\u2014";
14632
- var MS_PER_SECOND = 1e3;
14633
- var MS_PER_MINUTE = 60 * MS_PER_SECOND;
14634
- var MS_PER_HOUR = 60 * MS_PER_MINUTE;
14635
- var formatDuration = (ms) => {
14636
- if (ms === void 0) return EM_DASH;
14637
- if (ms < 0) return `${String(ms)}ms`;
14638
- if (ms < MS_PER_SECOND) return `${String(ms)}ms`;
14639
- if (ms < MS_PER_MINUTE) {
14640
- return `${String(Math.floor(ms / MS_PER_SECOND))}s`;
14641
- }
14642
- if (ms < MS_PER_HOUR) {
14643
- const minutes2 = Math.floor(ms / MS_PER_MINUTE);
14644
- const seconds = Math.floor(ms % MS_PER_MINUTE / MS_PER_SECOND);
14645
- return seconds > 0 ? `${String(minutes2)}m ${String(seconds)}s` : `${String(minutes2)}m`;
14646
- }
14647
- const hours = Math.floor(ms / MS_PER_HOUR);
14648
- const minutes = Math.floor(ms % MS_PER_HOUR / MS_PER_MINUTE);
14649
- return minutes > 0 ? `${String(hours)}h ${String(minutes)}m` : `${String(hours)}h`;
14650
- };
14645
+ var EM_DASH2 = "\u2014";
14651
14646
  var SHA_DISPLAY_LENGTH = 7;
14652
14647
  var appendSubsection = (lines, heading, entries) => {
14653
14648
  if (entries.length === 0) return;
@@ -14734,7 +14729,7 @@ var appendOutcomeDetail = (lines, input) => {
14734
14729
  lines.push("");
14735
14730
  };
14736
14731
  var renderJournalEntry = (input) => {
14737
- const sha = input.commitSha !== void 0 ? input.commitSha.slice(0, SHA_DISPLAY_LENGTH) : EM_DASH;
14732
+ const sha = input.commitSha !== void 0 ? input.commitSha.slice(0, SHA_DISPLAY_LENGTH) : EM_DASH2;
14738
14733
  const outcome = input.outcome.trim().length > 0 ? neutralizeProseHeadings(input.outcome.trim()) : "_(no outcome paragraph)_";
14739
14734
  const lines = [];
14740
14735
  lines.push("");
@@ -15319,7 +15314,7 @@ var validateSignalsFile = async (outputDir, contract) => {
15319
15314
  }
15320
15315
  const wrapper = current;
15321
15316
  const inner = wrapper.signals;
15322
- const defaulted = defaultMissingTimestamps(inner);
15317
+ const defaulted = renameNarrativeBodyToText(defaultMissingTimestamps(inner));
15323
15318
  const parsed = contract.signalsSchema.safeParse(defaulted);
15324
15319
  if (!parsed.success) {
15325
15320
  return Result.error(
@@ -15337,6 +15332,18 @@ var describeJsonError = (cause) => {
15337
15332
  if (cause instanceof Error) return cause.message;
15338
15333
  return String(cause);
15339
15334
  };
15335
+ var NARRATIVE_SIGNAL_KINDS = /* @__PURE__ */ new Set(["learning", "note", "decision"]);
15336
+ var renameNarrativeBodyToText = (inner) => {
15337
+ if (!Array.isArray(inner)) return inner;
15338
+ return inner.map((sig) => {
15339
+ if (typeof sig !== "object" || sig === null) return sig;
15340
+ const s = sig;
15341
+ if (typeof s.type !== "string" || !NARRATIVE_SIGNAL_KINDS.has(s.type)) return sig;
15342
+ if (typeof s.text === "string" || typeof s.body !== "string") return sig;
15343
+ const { body, ...rest } = s;
15344
+ return { ...rest, text: body };
15345
+ });
15346
+ };
15340
15347
  var defaultMissingTimestamps = (inner) => {
15341
15348
  if (!Array.isArray(inner)) return inner;
15342
15349
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -15427,6 +15434,16 @@ var refineExampleSignals = [
15427
15434
  type: "decision",
15428
15435
  text: "Scoped export to CSV only for v1; XLSX deferred to a follow-up ticket.",
15429
15436
  timestamp: EXAMPLE_TS
15437
+ },
15438
+ {
15439
+ type: "learning",
15440
+ text: "The export path already streams; the ticket must not assume an in-memory buffer.",
15441
+ timestamp: EXAMPLE_TS
15442
+ },
15443
+ {
15444
+ type: "note",
15445
+ text: "The user ruled column ordering out of scope \u2014 captured as a deferred item rather than an AC.",
15446
+ timestamp: EXAMPLE_TS
15430
15447
  }
15431
15448
  ];
15432
15449
  var refineOutputContract = {
@@ -15654,11 +15671,29 @@ var assertTemplateKeysFilled = (rendered, template, partialBodies, values, where
15654
15671
  };
15655
15672
 
15656
15673
  // src/integration/ai/prompts/_engine/build-prompt.ts
15674
+ var findPartialParameterCollision = (def, partialPlaceholders) => {
15675
+ for (const [field, rawSpec] of Object.entries(def.parameters)) {
15676
+ if (partialPlaceholders.has(rawSpec.placeholder)) return [field, rawSpec.placeholder];
15677
+ }
15678
+ return void 0;
15679
+ };
15657
15680
  var buildPrompt = async (loader, def, input) => {
15658
15681
  const template = await loader.load(def.templateName);
15659
15682
  if (!template.ok) return Result.error(template.error);
15660
15683
  const values = {};
15661
15684
  const partialBodies = [];
15685
+ const partialPlaceholders = new Set(def.partials !== void 0 ? Object.keys(def.partials) : []);
15686
+ const collision = findPartialParameterCollision(def, partialPlaceholders);
15687
+ if (collision !== void 0) {
15688
+ const [field, placeholder] = collision;
15689
+ return Result.error(
15690
+ new ValidationError({
15691
+ field,
15692
+ value: placeholder,
15693
+ message: `buildPrompt(${def.templateName}): parameter '${field}' declares placeholder {{${placeholder}}}, which collides with an auto-loaded partial slot of the same name \u2014 rename one of them.`
15694
+ })
15695
+ );
15696
+ }
15662
15697
  if (def.partials !== void 0) {
15663
15698
  for (const [placeholder, name] of Object.entries(def.partials)) {
15664
15699
  const partial = await loader.load(name);
@@ -16840,6 +16875,21 @@ var planExampleSignals = [
16840
16875
  type: "task-plan",
16841
16876
  tasksJson: '[{"name":"Wire export endpoint","ticketRef":"<ticket-uuid>","projectPath":"/abs/repo","steps":["..."],"verificationCriteria":["..."]}]',
16842
16877
  timestamp: EXAMPLE_TS2
16878
+ },
16879
+ {
16880
+ type: "decision",
16881
+ text: "Split the export work into endpoint + serializer tasks so the shared file is owned by exactly one task.",
16882
+ timestamp: EXAMPLE_TS2
16883
+ },
16884
+ {
16885
+ type: "learning",
16886
+ text: "The repo runs its integration suite from a separate config; the root test command does not cover it.",
16887
+ timestamp: EXAMPLE_TS2
16888
+ },
16889
+ {
16890
+ type: "note",
16891
+ text: "Two approved tickets touch the same route file; sequenced them via blockedBy rather than splitting the file.",
16892
+ timestamp: EXAMPLE_TS2
16843
16893
  }
16844
16894
  ];
16845
16895
  var planOutputContract = {
@@ -18509,7 +18559,6 @@ var branchPreflightLeaf = (deps, opts, name = "branch-preflight") => leaf(name,
18509
18559
  });
18510
18560
 
18511
18561
  // src/application/flows/implement/leaves/build-task-workspace.ts
18512
- import { promises as fs21 } from "fs";
18513
18562
  import { join as join44 } from "path";
18514
18563
 
18515
18564
  // src/business/task/compose-criteria-history.ts
@@ -18770,6 +18819,16 @@ var generatorExampleSignals = [
18770
18819
  appliesTo: "config / io layer",
18771
18820
  timestamp: EXAMPLE_TS3
18772
18821
  },
18822
+ {
18823
+ type: "decision",
18824
+ text: "Centralised the parse in one helper rather than patching both call sites, to avoid future drift.",
18825
+ timestamp: EXAMPLE_TS3
18826
+ },
18827
+ {
18828
+ type: "note",
18829
+ text: "Left the unrelated typo at src/baz.ts:8 alone \u2014 out of scope for this task.",
18830
+ timestamp: EXAMPLE_TS3
18831
+ },
18773
18832
  { type: "task-verified", output: "$ <project test command>\n... 42 passed", timestamp: EXAMPLE_TS3 },
18774
18833
  {
18775
18834
  type: COMMIT_MESSAGE_KIND,
@@ -18802,22 +18861,6 @@ ${body}
18802
18861
  };
18803
18862
 
18804
18863
  // src/application/flows/implement/leaves/build-task-workspace.ts
18805
- var writeOrError = async (path2, content) => {
18806
- try {
18807
- await fs21.mkdir(path2.slice(0, path2.lastIndexOf("/")), { recursive: true });
18808
- await fs21.writeFile(path2, content, "utf8");
18809
- return Result.ok(void 0);
18810
- } catch (cause) {
18811
- return Result.error(
18812
- new StorageError({
18813
- subCode: "io",
18814
- message: `failed to write task workspace file: ${path2}`,
18815
- path: path2,
18816
- cause
18817
- })
18818
- );
18819
- }
18820
- };
18821
18864
  var buildTaskWorkspaceLeaf = (deps, opts, taskId) => leaf(`build-task-workspace-${String(taskId)}`, {
18822
18865
  useCase: {
18823
18866
  execute: async (input) => {
@@ -18826,6 +18869,10 @@ var buildTaskWorkspaceLeaf = (deps, opts, taskId) => leaf(`build-task-workspace-
18826
18869
  const previewOutputDir = AbsolutePath.parse(join44(workspaceRoot, "rounds", "1", "generator"));
18827
18870
  if (!previewOutputDir.ok) return Result.error(previewOutputDir.error);
18828
18871
  const contractPath = join44(workspaceRoot, "contract.md");
18872
+ const parsedContractPath = AbsolutePath.parse(contractPath);
18873
+ if (!parsedContractPath.ok) return Result.error(parsedContractPath.error);
18874
+ const parsedPromptPath = AbsolutePath.parse(join44(workspaceRoot, "prompt.md"));
18875
+ if (!parsedPromptPath.ok) return Result.error(parsedPromptPath.error);
18829
18876
  const prompt = await buildImplementPrompt(deps.templateLoader, {
18830
18877
  task: input.task,
18831
18878
  projectPath: String(opts.cwd),
@@ -18836,9 +18883,9 @@ var buildTaskWorkspaceLeaf = (deps, opts, taskId) => leaf(`build-task-workspace-
18836
18883
  ...opts.verifyScript !== void 0 ? { verifyScript: opts.verifyScript } : {}
18837
18884
  });
18838
18885
  if (!prompt.ok) return Result.error(prompt.error);
18839
- const wrotePrompt = await writeOrError(join44(workspaceRoot, "prompt.md"), String(prompt.value));
18886
+ const wrotePrompt = await deps.writeFile(parsedPromptPath.value, String(prompt.value));
18840
18887
  if (!wrotePrompt.ok) return Result.error(wrotePrompt.error);
18841
- const wroteContract = await writeOrError(contractPath, renderContractMd(input.task));
18888
+ const wroteContract = await deps.writeFile(parsedContractPath.value, renderContractMd(input.task));
18842
18889
  if (!wroteContract.ok) return Result.error(wroteContract.error);
18843
18890
  log.debug("task workspace built", { taskId: input.task.id, workspaceRoot });
18844
18891
  const parsedRoot = AbsolutePath.parse(workspaceRoot);
@@ -19259,6 +19306,7 @@ var markTaskDone = (task, now) => {
19259
19306
  ...guard2.value.escalatedFromModel !== void 0 ? { escalatedFromModel: guard2.value.escalatedFromModel } : {},
19260
19307
  ...guard2.value.escalatedToModel !== void 0 ? { escalatedToModel: guard2.value.escalatedToModel } : {},
19261
19308
  ...guard2.value.escalatedToEffort !== void 0 ? { escalatedToEffort: guard2.value.escalatedToEffort } : {},
19309
+ ...guard2.value.escalatedToEvaluatorEffort !== void 0 ? { escalatedToEvaluatorEffort: guard2.value.escalatedToEvaluatorEffort } : {},
19262
19310
  status: "done",
19263
19311
  attempts,
19264
19312
  finalAttemptN: verified.n
@@ -19303,6 +19351,11 @@ var recordTaskEffortEscalation = (task, toEffort) => {
19303
19351
  if (!to.ok) return Result.error(to.error);
19304
19352
  return Result.ok({ ...task, escalatedToEffort: to.value });
19305
19353
  };
19354
+ var recordTaskEvaluatorEffortEscalation = (task, toEffort) => {
19355
+ const to = parseRequiredString("task.escalatedToEvaluatorEffort", toEffort);
19356
+ if (!to.ok) return Result.error(to.error);
19357
+ return Result.ok({ ...task, escalatedToEvaluatorEffort: to.value });
19358
+ };
19306
19359
 
19307
19360
  // src/business/task/escalation-policy.ts
19308
19361
  var BANNER_SHOW_EVENT = "banner-show";
@@ -19326,11 +19379,14 @@ var decideEscalation = (props) => {
19326
19379
  if (nudgedAtTop) return { kind: "topped-out", model: props.generatorModel };
19327
19380
  const effortTarget = nextEffortRung(props.generatorProvider, props.generatorModel, props.generatorEffort);
19328
19381
  if (effortTarget !== void 0) {
19382
+ const evaluatorEffortTarget = props.evaluatorModel !== void 0 ? nextEffortRung(props.evaluatorProvider, props.evaluatorModel, props.evaluatorEffort) : void 0;
19383
+ const evaluatorCarry = evaluatorEffortTarget !== void 0 ? { evaluator: { from: props.evaluatorEffort ?? "default", to: evaluatorEffortTarget } } : {};
19329
19384
  return {
19330
19385
  kind: "escalate-effort",
19331
19386
  model: props.generatorModel,
19332
19387
  from: props.generatorEffort ?? "default",
19333
- to: effortTarget
19388
+ to: effortTarget,
19389
+ ...evaluatorCarry
19334
19390
  };
19335
19391
  }
19336
19392
  return { kind: "nudge", currentModel: props.generatorModel };
@@ -19402,11 +19458,12 @@ var applyEscalation = (props) => {
19402
19458
  return Result.ok({ task: stamped.value });
19403
19459
  }
19404
19460
  case "escalate-effort": {
19461
+ const evaluatorClause = decision.evaluator !== void 0 ? `; evaluator effort: ${decision.evaluator.from} \u2192 ${decision.evaluator.to}` : "";
19405
19462
  eventBus.publish({
19406
19463
  type: BANNER_SHOW_EVENT,
19407
19464
  id: bannerId,
19408
19465
  tier: "info",
19409
- message: `raised generator effort on '${decision.model}': ${decision.from} \u2192 ${decision.to}`,
19466
+ message: `raised generator effort on '${decision.model}': ${decision.from} \u2192 ${decision.to}${evaluatorClause}`,
19410
19467
  cause,
19411
19468
  at: now
19412
19469
  });
@@ -19415,7 +19472,8 @@ var applyEscalation = (props) => {
19415
19472
  model: decision.model,
19416
19473
  from: decision.from,
19417
19474
  to: decision.to,
19418
- reason: trigger
19475
+ reason: trigger,
19476
+ ...decision.evaluator !== void 0 ? { evaluatorFrom: decision.evaluator.from, evaluatorTo: decision.evaluator.to } : {}
19419
19477
  });
19420
19478
  return Result.ok({ task });
19421
19479
  }
@@ -19516,7 +19574,12 @@ var resolveEscalatableRemedy = (exit, props, cfg) => {
19516
19574
  // provider (or the row carries no effort dimension the caller could resolve) the policy never
19517
19575
  // returns `escalate-effort` and behaves exactly as before.
19518
19576
  generatorProvider: props.generatorProvider,
19519
- generatorEffort: props.generatorEffort
19577
+ generatorEffort: props.generatorEffort,
19578
+ // Evaluator lockstep context — same optionality: absent context means the policy never
19579
+ // returns an `evaluator` field on the decision.
19580
+ evaluatorProvider: props.evaluatorProvider,
19581
+ evaluatorModel: props.evaluatorModel,
19582
+ evaluatorEffort: props.evaluatorEffort
19520
19583
  });
19521
19584
  const applied = applyEscalation({
19522
19585
  task: props.task,
@@ -19536,6 +19599,11 @@ var resolveEscalatableRemedy = (exit, props, cfg) => {
19536
19599
  const stamped = recordTaskEffortEscalation(task, decision.to);
19537
19600
  if (!stamped.ok) return Result.error(stamped.error);
19538
19601
  task = stamped.value;
19602
+ if (decision.evaluator !== void 0) {
19603
+ const evaluatorStamped = recordTaskEvaluatorEffortEscalation(task, decision.evaluator.to);
19604
+ if (!evaluatorStamped.ok) return Result.error(evaluatorStamped.error);
19605
+ task = evaluatorStamped.value;
19606
+ }
19539
19607
  }
19540
19608
  const shouldFailAttempt = decision.kind === "escalate" || decision.kind === "escalate-effort" || decision.kind === "nudge";
19541
19609
  return Result.ok({
@@ -19632,7 +19700,10 @@ var finalizeGenEvalLeaf = (deps, taskId) => leaf(`finalize-gen-eval-${String(tas
19632
19700
  clock: deps.clock,
19633
19701
  generatorModel: input.generatorModel,
19634
19702
  ...input.generatorProvider !== void 0 ? { generatorProvider: input.generatorProvider } : {},
19635
- ...input.generatorEffort !== void 0 ? { generatorEffort: input.generatorEffort } : {}
19703
+ ...input.generatorEffort !== void 0 ? { generatorEffort: input.generatorEffort } : {},
19704
+ ...input.evaluatorProvider !== void 0 ? { evaluatorProvider: input.evaluatorProvider } : {},
19705
+ ...input.evaluatorModel !== void 0 ? { evaluatorModel: input.evaluatorModel } : {},
19706
+ ...input.evaluatorEffort !== void 0 ? { evaluatorEffort: input.evaluatorEffort } : {}
19636
19707
  })
19637
19708
  },
19638
19709
  input: (ctx) => {
@@ -19654,6 +19725,7 @@ var finalizeGenEvalLeaf = (deps, taskId) => leaf(`finalize-gen-eval-${String(tas
19654
19725
  }
19655
19726
  const generatorModel = ctx.currentTask.escalatedToModel ?? deps.configuredGeneratorModel;
19656
19727
  const generatorEffort = ctx.currentTask.escalatedToEffort ?? deps.configuredGeneratorEffort;
19728
+ const evaluatorEffort = ctx.currentTask.escalatedToEvaluatorEffort ?? deps.configuredEvaluatorEffort;
19657
19729
  return {
19658
19730
  task: ctx.currentTask,
19659
19731
  sprintId: ctx.sprintId,
@@ -19661,7 +19733,10 @@ var finalizeGenEvalLeaf = (deps, taskId) => leaf(`finalize-gen-eval-${String(tas
19661
19733
  turnsUsed: ctx.genEvalTurn ?? 0,
19662
19734
  generatorModel,
19663
19735
  ...deps.configuredGeneratorProvider !== void 0 ? { generatorProvider: deps.configuredGeneratorProvider } : {},
19664
- ...generatorEffort !== void 0 ? { generatorEffort } : {}
19736
+ ...generatorEffort !== void 0 ? { generatorEffort } : {},
19737
+ ...deps.configuredEvaluatorProvider !== void 0 ? { evaluatorProvider: deps.configuredEvaluatorProvider } : {},
19738
+ ...deps.configuredEvaluatorModel !== void 0 ? { evaluatorModel: deps.configuredEvaluatorModel } : {},
19739
+ ...evaluatorEffort !== void 0 ? { evaluatorEffort } : {}
19665
19740
  };
19666
19741
  },
19667
19742
  output: (ctx, out) => {
@@ -19799,7 +19874,7 @@ var computePlateauVerdict = (priorTurns, current, options) => {
19799
19874
 
19800
19875
  // src/application/flows/implement/leaves/evaluator.ts
19801
19876
  import { dirname as dirname17, join as join46 } from "path";
19802
- import { promises as fs23 } from "fs";
19877
+ import { promises as fs22 } from "fs";
19803
19878
 
19804
19879
  // src/business/task/turn-error-policy.ts
19805
19880
  var isRecoverableTurnError = (err) => err.code !== ErrorCode.Aborted && err.code !== ErrorCode.RateLimit;
@@ -20137,7 +20212,7 @@ var buildEvaluateContinuationPrompt = async (deps, input) => buildPrompt(deps, e
20137
20212
 
20138
20213
  // src/integration/ai/contract/_engine/corrective-retry.ts
20139
20214
  import { ZodError } from "zod";
20140
- var isCorrectableContractError = (err) => err.code !== ErrorCode.Aborted && err.code !== ErrorCode.RateLimit && err.code !== ErrorCode.MigrationGap;
20215
+ var isCorrectableContractError = (err) => err instanceof InvalidStateError || err instanceof ParseError;
20141
20216
  var MAX_ENUMERATED_ISSUES = 10;
20142
20217
  var correctiveBody = (err, signalsPath, selfContainedContext) => {
20143
20218
  const lines = [];
@@ -20208,6 +20283,14 @@ var validateSignalsFileWithCorrectiveRetry = async (deps, contract) => {
20208
20283
  return Result.ok({ signals: revalidated.value, nudgeCount: attempt });
20209
20284
  }
20210
20285
  lastErr = revalidated.error;
20286
+ if (!isCorrectableContractError(lastErr)) {
20287
+ log.warn("corrective retry produced a non-correctable error \u2014 self-blocking without a further nudge", {
20288
+ outputDir: String(deps.outputDir),
20289
+ attempt,
20290
+ error: lastErr.message
20291
+ });
20292
+ return Result.error(lastErr);
20293
+ }
20211
20294
  }
20212
20295
  log.warn("corrective retries exhausted \u2014 self-blocking", {
20213
20296
  outputDir: String(deps.outputDir),
@@ -20454,6 +20537,16 @@ var evaluatorExampleSignals = [
20454
20537
  ],
20455
20538
  critique: "Correctness: add edge-case handling for empty input at src/foo.ts:23.",
20456
20539
  timestamp: EXAMPLE_TS4
20540
+ },
20541
+ {
20542
+ type: "learning",
20543
+ text: "The suite needs a built fixture; a cold checkout fails the C1 command for reasons unrelated to the task.",
20544
+ timestamp: EXAMPLE_TS4
20545
+ },
20546
+ {
20547
+ type: "note",
20548
+ text: "C2 was checked by reading the call site \u2014 the criterion is manual, so no command was run.",
20549
+ timestamp: EXAMPLE_TS4
20457
20550
  }
20458
20551
  ];
20459
20552
  var evaluatorOutputContract = {
@@ -20469,7 +20562,7 @@ var evaluatorOutputContract = {
20469
20562
  };
20470
20563
 
20471
20564
  // src/application/flows/implement/leaves/round-artifacts.ts
20472
- import { promises as fs22 } from "fs";
20565
+ import { promises as fs21 } from "fs";
20473
20566
  import { join as join45 } from "path";
20474
20567
  var nextRoundNum = async (workspaceRoot) => {
20475
20568
  const entries = await listDir(join45(String(workspaceRoot), "rounds"));
@@ -20488,7 +20581,7 @@ var readRoundSessionId = async (workspaceRoot, round, role) => {
20488
20581
  const path2 = join45(String(workspaceRoot), "rounds", String(round), role, "session-id.txt");
20489
20582
  let content;
20490
20583
  try {
20491
- content = await fs22.readFile(path2, "utf8");
20584
+ content = await fs21.readFile(path2, "utf8");
20492
20585
  } catch {
20493
20586
  return void 0;
20494
20587
  }
@@ -20560,7 +20653,7 @@ var positiveCountCarry = (field, delta, prior) => delta > 0 ? { [field]: (prior
20560
20653
  // src/application/flows/implement/leaves/evaluator.ts
20561
20654
  var readCappedProgress = async (path2, currentTaskId, model) => {
20562
20655
  try {
20563
- return capProgressBody(await fs23.readFile(path2, "utf8"), {
20656
+ return capProgressBody(await fs22.readFile(path2, "utf8"), {
20564
20657
  currentTaskId,
20565
20658
  recentBudgetTokens: progressCapBudgetForModel(model)
20566
20659
  });
@@ -20619,7 +20712,7 @@ var makeEvaluatorReinvoke = (deps, args) => async (corrective, attempt) => {
20619
20712
  args.signalsFile,
20620
20713
  "evaluator",
20621
20714
  resume,
20622
- deps.effort,
20715
+ args.effectiveEffort,
20623
20716
  args.signal,
20624
20717
  bodyFile.ok ? bodyFile.value : void 0
20625
20718
  )
@@ -20647,7 +20740,8 @@ var makeEvaluatorCallEvaluate = (deps, args) => async (task) => {
20647
20740
  const initialBodyFile = AbsolutePath.parse(
20648
20741
  roundBodyPath(args.input.workspaceRoot, args.input.roundNum, "evaluator")
20649
20742
  );
20650
- const spawn3 = await deps.provider.generate(
20743
+ const effectiveEffort = task.escalatedToEvaluatorEffort ?? deps.effort;
20744
+ const spawn2 = await deps.provider.generate(
20651
20745
  implementSession(
20652
20746
  args.input.workspaceRoot,
20653
20747
  deps.cwd,
@@ -20657,12 +20751,12 @@ var makeEvaluatorCallEvaluate = (deps, args) => async (task) => {
20657
20751
  args.signalsFile,
20658
20752
  "evaluator",
20659
20753
  args.input.priorEvaluatorSessionId,
20660
- deps.effort,
20754
+ effectiveEffort,
20661
20755
  args.signal,
20662
20756
  initialBodyFile.ok ? initialBodyFile.value : void 0
20663
20757
  )
20664
20758
  );
20665
- if (!spawn3.ok) return Result.error(spawn3.error);
20759
+ if (!spawn2.ok) return Result.error(spawn2.error);
20666
20760
  const validated = await validateSignalsFileWithCorrectiveRetry(
20667
20761
  {
20668
20762
  outputDir: args.outputDir,
@@ -20684,6 +20778,7 @@ var makeEvaluatorCallEvaluate = (deps, args) => async (task) => {
20684
20778
  workspaceRoot: args.input.workspaceRoot,
20685
20779
  roundNum: args.input.roundNum,
20686
20780
  signalsFile: args.signalsFile,
20781
+ effectiveEffort,
20687
20782
  priorEvaluatorSessionId: args.input.priorEvaluatorSessionId,
20688
20783
  signal: args.signal
20689
20784
  })
@@ -20803,7 +20898,7 @@ var evaluatorLeaf = (deps, taskId) => leaf(`evaluator-${String(taskId)}`, {
20803
20898
 
20804
20899
  // src/application/flows/implement/leaves/generator.ts
20805
20900
  import { dirname as dirname18, join as join47 } from "path";
20806
- import { promises as fs25 } from "fs";
20901
+ import { promises as fs24 } from "fs";
20807
20902
 
20808
20903
  // src/business/task/run-generator-turn.ts
20809
20904
  var findTaskBlocked = (signals) => signals.find((s) => s.type === "task-blocked")?.reason;
@@ -20970,7 +21065,8 @@ var composeDimensionTrajectory = (input) => {
20970
21065
  const priorFailed = failedDimensions(prior.evaluation);
20971
21066
  const fixed = sortedLimited([...priorFailed].filter((d) => !latestFailed.has(d)));
20972
21067
  const newlyFailing = sortedLimited([...latestFailed].filter((d) => !priorFailed.has(d)));
20973
- const stillFailing = sortedLimited([...latestFailed].filter((d) => priorFailed.has(d)));
21068
+ const stillFailingAll = [...latestFailed].filter((d) => priorFailed.has(d));
21069
+ const stillFailing = sortedLimited(stillFailingAll);
20974
21070
  const lines = [];
20975
21071
  for (const d of fixed) {
20976
21072
  lines.push(`- ${d}: fixed since last round \u2014 keep it passing.`);
@@ -20984,7 +21080,7 @@ var composeDimensionTrajectory = (input) => {
20984
21080
  }
20985
21081
  if (lines.length === 0) return "";
20986
21082
  const threshold = Math.max(2, Math.trunc(input.plateauThreshold));
20987
- const longestStall = stillFailing.reduce((max, d) => Math.max(max, consecutiveFailing(history, d)), 0);
21083
+ const longestStall = stillFailingAll.reduce((max, d) => Math.max(max, consecutiveFailing(history, d)), 0);
20988
21084
  const pressure = longestStall >= threshold - 1 ? [
20989
21085
  "",
20990
21086
  `Round ${String(input.roundNum)} of ${String(input.maxTurns)}; ${String(longestStall)} stalled round(s) \u2014 the harness exits this loop at ${String(threshold)} consecutive stalled rounds and escalates. Do NOT repeat the previous approach on a still-failing dimension; step back and try a fundamentally different fix.`
@@ -21103,7 +21199,7 @@ var formatRetryFeedback = (task, logTail) => {
21103
21199
  };
21104
21200
 
21105
21201
  // src/integration/io/read-log-tail.ts
21106
- import { promises as fs24 } from "fs";
21202
+ import { promises as fs23 } from "fs";
21107
21203
  var DEFAULT_LOG_TAIL_BYTES = 4096;
21108
21204
  var createFsLogTailReader = () => {
21109
21205
  return async (path2, maxBytes = DEFAULT_LOG_TAIL_BYTES) => {
@@ -21111,7 +21207,7 @@ var createFsLogTailReader = () => {
21111
21207
  if (cap === 0) return "";
21112
21208
  let handle;
21113
21209
  try {
21114
- handle = await fs24.open(String(path2), "r");
21210
+ handle = await fs23.open(String(path2), "r");
21115
21211
  const stat = await handle.stat();
21116
21212
  const size = stat.size;
21117
21213
  if (size <= 0) return "";
@@ -21142,7 +21238,7 @@ var countTurnActionKinds = (out) => {
21142
21238
  };
21143
21239
  var readCappedProgress2 = async (path2, currentTaskId, model) => {
21144
21240
  try {
21145
- return capProgressBody(await fs25.readFile(path2, "utf8"), {
21241
+ return capProgressBody(await fs24.readFile(path2, "utf8"), {
21146
21242
  currentTaskId,
21147
21243
  recentBudgetTokens: progressCapBudgetForModel(model)
21148
21244
  });
@@ -21307,7 +21403,7 @@ var makeGeneratorCallImplement = (deps, taskId, args) => async (task) => {
21307
21403
  const effectiveModel = task.escalatedToModel ?? deps.model;
21308
21404
  const effectiveEffort = task.escalatedToEffort ?? deps.effort;
21309
21405
  const initialBodyFile = AbsolutePath.parse(roundBodyPath(args.input.workspaceRoot, args.roundNum, "generator"));
21310
- const spawn3 = await deps.provider.generate(
21406
+ const spawn2 = await deps.provider.generate(
21311
21407
  implementSession(
21312
21408
  args.input.workspaceRoot,
21313
21409
  deps.cwd,
@@ -21322,7 +21418,7 @@ var makeGeneratorCallImplement = (deps, taskId, args) => async (task) => {
21322
21418
  initialBodyFile.ok ? initialBodyFile.value : void 0
21323
21419
  )
21324
21420
  );
21325
- if (!spawn3.ok) return Result.error(spawn3.error);
21421
+ if (!spawn2.ok) return Result.error(spawn2.error);
21326
21422
  const validated = await validateSignalsFileWithCorrectiveRetry(
21327
21423
  {
21328
21424
  outputDir: args.outputDir,
@@ -22668,13 +22764,13 @@ var quarantineRetryDiffLeaf = (deps, opts, taskId) => leaf(`quarantine-retry-dif
22668
22764
  var isRedVerifyRetry = (ctx) => ctx.lastShouldFailAttempt === true && ctx.lastBlockReason !== void 0;
22669
22765
 
22670
22766
  // src/application/flows/implement/leaves/progress-journal.ts
22671
- import { promises as fs26 } from "fs";
22767
+ import { promises as fs25 } from "fs";
22672
22768
 
22673
22769
  // src/business/sprint/render-sprint-state-header.ts
22674
- var EM_DASH2 = "\u2014";
22770
+ var EM_DASH3 = "\u2014";
22675
22771
  var cell = (text) => neutralizeProseHeadings(sanitizeInline(text));
22676
22772
  var isStale = (task) => task.status === "in_progress" && task.attemptCount > 0;
22677
- var orEmDash = (value) => value !== null && value.length > 0 ? value : EM_DASH2;
22773
+ var orEmDash = (value) => value !== null && value.length > 0 ? value : EM_DASH3;
22678
22774
  var renderIdentity = (input) => [
22679
22775
  `# Sprint: ${cell(input.sprintName)}`,
22680
22776
  "",
@@ -22694,7 +22790,7 @@ var renderBlockers = (tasks) => {
22694
22790
  const blockers = tasks.filter((t) => t.status === "blocked");
22695
22791
  if (blockers.length === 0) return [];
22696
22792
  const bullets = blockers.map((t) => {
22697
- const reason = t.blockedReason !== void 0 && t.blockedReason.trim().length > 0 ? cell(t.blockedReason) : EM_DASH2;
22793
+ const reason = t.blockedReason !== void 0 && t.blockedReason.trim().length > 0 ? cell(t.blockedReason) : EM_DASH3;
22698
22794
  return `- ${cell(t.name)}: ${reason}`;
22699
22795
  });
22700
22796
  return ["## Blockers", "", ...bullets, ""];
@@ -22707,7 +22803,7 @@ var renderStale = (tasks) => {
22707
22803
  );
22708
22804
  return ["## Stale tasks", "", ...bullets, ""];
22709
22805
  };
22710
- var passesCell = (task) => task.criteriaTotal === 0 ? EM_DASH2 : `${String(task.criteriaPassed)}/${String(task.criteriaTotal)}`;
22806
+ var passesCell = (task) => task.criteriaTotal === 0 ? EM_DASH3 : `${String(task.criteriaPassed)}/${String(task.criteriaTotal)}`;
22711
22807
  var renderTaskTable = (tasks) => {
22712
22808
  if (tasks.length === 0) return ["## Tasks", "", "_No tasks planned yet._", ""];
22713
22809
  const rows = tasks.map((t) => `| ${cell(t.name)} | ${t.status} | ${passesCell(t)} |`);
@@ -22845,7 +22941,7 @@ var buildAttemptSection = (input, totalRounds, clock) => {
22845
22941
  };
22846
22942
  var readExisting = async (path2) => {
22847
22943
  try {
22848
- return await fs26.readFile(String(path2), "utf8");
22944
+ return await fs25.readFile(String(path2), "utf8");
22849
22945
  } catch {
22850
22946
  return "";
22851
22947
  }
@@ -23085,16 +23181,16 @@ ${output.slice(output.length - tail)}`;
23085
23181
  };
23086
23182
 
23087
23183
  // src/business/task/render-round-outcome.ts
23088
- var EM_DASH3 = "\u2014";
23184
+ var EM_DASH4 = "\u2014";
23089
23185
  var renderRoundOutcome = (input) => {
23090
23186
  const lines = [];
23091
23187
  lines.push(`# Round ${String(input.roundN)} \xB7 attempt ${String(input.attemptN)}`);
23092
23188
  lines.push("");
23093
- lines.push(`- generator session: ${input.generatorSessionId ?? EM_DASH3}`);
23094
- lines.push(`- evaluator session: ${input.evaluatorSessionId ?? EM_DASH3}`);
23095
- lines.push(`- duration: ${formatDuration2(input.durationMs)}`);
23189
+ lines.push(`- generator session: ${input.generatorSessionId ?? EM_DASH4}`);
23190
+ lines.push(`- evaluator session: ${input.evaluatorSessionId ?? EM_DASH4}`);
23191
+ lines.push(`- duration: ${formatDuration(input.durationMs)}`);
23096
23192
  lines.push(`- verdict: ${input.verdict}`);
23097
- lines.push(`- commit: ${input.attempt.commitSha !== void 0 ? String(input.attempt.commitSha) : EM_DASH3}`);
23193
+ lines.push(`- commit: ${input.attempt.commitSha !== void 0 ? String(input.attempt.commitSha) : EM_DASH4}`);
23098
23194
  const dimensionsBlock = renderDimensions(input.evaluation, input.attempt.evaluation);
23099
23195
  if (dimensionsBlock !== void 0) {
23100
23196
  lines.push("");
@@ -23166,25 +23262,6 @@ var formatFailedDimensions = (evaluation) => {
23166
23262
  };
23167
23263
  var SHA_DISPLAY_LENGTH2 = 7;
23168
23264
  var shortSha = (sha) => sha.slice(0, SHA_DISPLAY_LENGTH2);
23169
- var MS_PER_SECOND2 = 1e3;
23170
- var MS_PER_MINUTE2 = 60 * MS_PER_SECOND2;
23171
- var MS_PER_HOUR2 = 60 * MS_PER_MINUTE2;
23172
- var formatDuration2 = (ms) => {
23173
- if (ms === void 0) return EM_DASH3;
23174
- if (ms < 0) return `${String(ms)}ms`;
23175
- if (ms < MS_PER_SECOND2) return `${String(ms)}ms`;
23176
- if (ms < MS_PER_MINUTE2) {
23177
- return `${String(Math.floor(ms / MS_PER_SECOND2))}s`;
23178
- }
23179
- if (ms < MS_PER_HOUR2) {
23180
- const minutes2 = Math.floor(ms / MS_PER_MINUTE2);
23181
- const seconds = Math.floor(ms % MS_PER_MINUTE2 / MS_PER_SECOND2);
23182
- return seconds > 0 ? `${String(minutes2)}m ${String(seconds)}s` : `${String(minutes2)}m`;
23183
- }
23184
- const hours = Math.floor(ms / MS_PER_HOUR2);
23185
- const minutes = Math.floor(ms % MS_PER_HOUR2 / MS_PER_MINUTE2);
23186
- return minutes > 0 ? `${String(hours)}h ${String(minutes)}m` : `${String(hours)}h`;
23187
- };
23188
23265
 
23189
23266
  // src/application/flows/implement/leaves/settle-attempt.ts
23190
23267
  var settleAttemptLeaf = (deps, opts, taskId) => {
@@ -23594,7 +23671,7 @@ var createPerTaskSubchain = (deps, opts, task, repo, readConfig) => {
23594
23671
  )
23595
23672
  ] : [],
23596
23673
  buildTaskWorkspaceLeaf(
23597
- { templateLoader: deps.templateLoader, logger: deps.logger },
23674
+ { templateLoader: deps.templateLoader, logger: deps.logger, writeFile: deps.writeFile },
23598
23675
  {
23599
23676
  sprintDir: opts.sprintDir,
23600
23677
  cwd: repo.path,
@@ -23702,7 +23779,14 @@ var createPerTaskSubchain = (deps, opts, task, repo, readConfig) => {
23702
23779
  // before spending the nudge. `providerId` is the resolved provider enum string;
23703
23780
  // `nextEffortRung` skips gracefully for any non-effort provider or model.
23704
23781
  configuredGeneratorProvider: opts.generator.providerId,
23705
- ...opts.generator.effort !== void 0 ? { configuredGeneratorEffort: opts.generator.effort } : {}
23782
+ ...opts.generator.effort !== void 0 ? { configuredGeneratorEffort: opts.generator.effort } : {},
23783
+ // Same lockstep wiring for the EVALUATOR role: the policy computes its bump
23784
+ // independently against this triple, never copying the generator's target. The
23785
+ // evaluator MODEL is never escalated, but it still rides through so `nextEffortRung`
23786
+ // can classify the evaluator's own effort ladder.
23787
+ configuredEvaluatorProvider: opts.evaluator.providerId,
23788
+ configuredEvaluatorModel: opts.evaluator.model,
23789
+ ...opts.evaluator.effort !== void 0 ? { configuredEvaluatorEffort: opts.evaluator.effort } : {}
23706
23790
  },
23707
23791
  taskId
23708
23792
  ),
@@ -24835,6 +24919,16 @@ var createPublishSignal = (eventBus, source, taskId) => (signal) => {
24835
24919
  eventBus.publish({ type: "ai-signal", signal, source, ...taskId !== void 0 ? { taskId } : {} });
24836
24920
  };
24837
24921
 
24922
+ // src/application/flows/implement/sprint-scoped-projection.ts
24923
+ var projectSprintScopedFields = (ctx) => ({
24924
+ sprintId: ctx.sprintId,
24925
+ sprint: ctx.sprint,
24926
+ execution: ctx.execution,
24927
+ progressFile: ctx.progressFile,
24928
+ setupVerifiedRepoIdsThisRun: ctx.setupVerifiedRepoIdsThisRun,
24929
+ priorLearnings: ctx.priorLearnings
24930
+ });
24931
+
24838
24932
  // src/application/flows/implement/merge-wave.ts
24839
24933
  var branchSettled = (outcome) => outcome.status === "completed" || outcome.error !== void 0;
24840
24934
  var mergeImplementWave = (base, outcomes) => {
@@ -24845,13 +24939,7 @@ var mergeImplementWave = (base, outcomes) => {
24845
24939
  }
24846
24940
  const tasks = base.tasks?.map((t) => byId.get(t.id) ?? t);
24847
24941
  return {
24848
- // sprint-scoped → base
24849
- sprintId: base.sprintId,
24850
- ...base.sprint !== void 0 ? { sprint: base.sprint } : {},
24851
- ...base.execution !== void 0 ? { execution: base.execution } : {},
24852
- ...base.progressFile !== void 0 ? { progressFile: base.progressFile } : {},
24853
- ...base.setupVerifiedRepoIdsThisRun !== void 0 ? { setupVerifiedRepoIdsThisRun: base.setupVerifiedRepoIdsThisRun } : {},
24854
- ...base.priorLearnings !== void 0 ? { priorLearnings: base.priorLearnings } : {},
24942
+ ...projectSprintScopedFields(base),
24855
24943
  // task list → overlay
24856
24944
  ...tasks !== void 0 ? { tasks } : {}
24857
24945
  // per-task + signal-accum classes intentionally omitted → undefined in the merged ctx.
@@ -24859,17 +24947,8 @@ var mergeImplementWave = (base, outcomes) => {
24859
24947
  };
24860
24948
  var forkCtx = (base, repo, worktreePath) => {
24861
24949
  const ctx = {
24862
- // sprint-scoped → base
24863
- sprintId: base.sprintId,
24864
- ...base.sprint !== void 0 ? { sprint: base.sprint } : {},
24865
- ...base.execution !== void 0 ? { execution: base.execution } : {},
24866
- ...base.progressFile !== void 0 ? { progressFile: base.progressFile } : {},
24867
- ...base.tasks !== void 0 ? { tasks: base.tasks } : {},
24868
- // run-scoped (like `execution`): the set of repos this launch's setup verified survives the
24869
- // fork so a parallel branch's first pre-task-verify can also take the fresh-setup skip.
24870
- ...base.setupVerifiedRepoIdsThisRun !== void 0 ? { setupVerifiedRepoIdsThisRun: base.setupVerifiedRepoIdsThisRun } : {},
24871
- // run-scoped cross-sprint memory: every branch's generator reads the same loaded ledger.
24872
- ...base.priorLearnings !== void 0 ? { priorLearnings: base.priorLearnings } : {}
24950
+ ...projectSprintScopedFields(base),
24951
+ ...base.tasks !== void 0 ? { tasks: base.tasks } : {}
24873
24952
  // per-task + signal-accum classes cleared (omitted → undefined); `priorPostVerifyOutcome`
24874
24953
  // dropped (accepted cost). `expectedBranch` is intentionally NOT set here — see the docstring:
24875
24954
  // the branch element omits `branch-preflight`, so leaving it `undefined` is correct.
@@ -25719,7 +25798,7 @@ var launchImplement = async (ctx) => {
25719
25798
  import { join as join60 } from "path";
25720
25799
 
25721
25800
  // src/application/flows/review/leaves/ensure-feedback-file.ts
25722
- import { promises as fs27 } from "fs";
25801
+ import { promises as fs26 } from "fs";
25723
25802
 
25724
25803
  // src/business/feedback/md-parser.ts
25725
25804
  var ROUND_HEADING_RE = /^##\s+Round\s+(\d+)\s*$/;
@@ -25794,12 +25873,12 @@ var ensureFeedbackFileLeaf = (feedbackFile) => leaf("ensure-feedback-file", {
25794
25873
  useCase: {
25795
25874
  execute: async (path2) => {
25796
25875
  try {
25797
- await fs27.access(String(path2));
25876
+ await fs26.access(String(path2));
25798
25877
  return Result.ok(void 0);
25799
25878
  } catch {
25800
25879
  }
25801
25880
  try {
25802
- await fs27.writeFile(String(path2), TEMPLATE, { flag: "wx" });
25881
+ await fs26.writeFile(String(path2), TEMPLATE, { flag: "wx" });
25803
25882
  return Result.ok(void 0);
25804
25883
  } catch (cause) {
25805
25884
  if (typeof cause === "object" && cause !== null && cause.code === "EEXIST") {
@@ -25821,7 +25900,7 @@ var ensureFeedbackFileLeaf = (feedbackFile) => leaf("ensure-feedback-file", {
25821
25900
  });
25822
25901
 
25823
25902
  // src/application/flows/review/leaves/review-round.ts
25824
- import { promises as fs28 } from "fs";
25903
+ import { promises as fs27 } from "fs";
25825
25904
  import { join as join56 } from "path";
25826
25905
 
25827
25906
  // src/business/feedback/apply-feedback.ts
@@ -26004,7 +26083,7 @@ var reviewRoundOutputContract = {
26004
26083
  var readProgressSnippet = async (path2) => {
26005
26084
  if (path2 === void 0) return "_(no progress file)_";
26006
26085
  try {
26007
- const content = await fs28.readFile(String(path2), "utf8");
26086
+ const content = await fs27.readFile(String(path2), "utf8");
26008
26087
  return content.length > 4e3 ? `${content.slice(0, 4e3)}
26009
26088
  [truncated]` : content;
26010
26089
  } catch {
@@ -26020,7 +26099,7 @@ ${renderEmptyRound(nextIndex)}${ROUND_SEPARATOR}
26020
26099
  };
26021
26100
  var deriveActiveRoundIndex = async (feedbackFile) => {
26022
26101
  try {
26023
- const raw = await fs28.readFile(String(feedbackFile), "utf8");
26102
+ const raw = await fs27.readFile(String(feedbackFile), "utf8");
26024
26103
  const rounds = parseFeedbackMd(raw);
26025
26104
  return rounds.at(-1)?.index ?? 1;
26026
26105
  } catch {
@@ -26029,7 +26108,7 @@ var deriveActiveRoundIndex = async (feedbackFile) => {
26029
26108
  };
26030
26109
  var writeRoundBody = async (path2, body) => {
26031
26110
  try {
26032
- const content = await fs28.readFile(String(path2), "utf8");
26111
+ const content = await fs27.readFile(String(path2), "utf8");
26033
26112
  const lastMarker = content.lastIndexOf(MARKER_COMMENT);
26034
26113
  if (lastMarker === -1) {
26035
26114
  return Result.error(
@@ -26069,7 +26148,7 @@ var allocateRoundPaths = (reviewRoot, roundIndex) => {
26069
26148
  };
26070
26149
  var ensureRoundDir = async (dir) => {
26071
26150
  try {
26072
- await fs28.mkdir(String(dir), { recursive: true });
26151
+ await fs27.mkdir(String(dir), { recursive: true });
26073
26152
  return Result.ok(void 0);
26074
26153
  } catch (cause) {
26075
26154
  return Result.error(
@@ -26104,7 +26183,7 @@ var reviewRoundLeaf = (deps, opts) => leaf(LEAF_NAME3, {
26104
26183
  if (!wrote.ok) return Result.error(wrote.error);
26105
26184
  return Result.ok(void 0);
26106
26185
  },
26107
- readFeedbackFile: () => fs28.readFile(String(input.feedbackFile), "utf8"),
26186
+ readFeedbackFile: () => fs27.readFile(String(input.feedbackFile), "utf8"),
26108
26187
  readProgressSnippet: () => readProgressSnippet(input.progressFile),
26109
26188
  buildPrompt: async (params) => {
26110
26189
  const outputContractSection = renderContractSectionFor(reviewRoundOutputContract, paths.value.outputDir);
@@ -26132,7 +26211,7 @@ var reviewRoundLeaf = (deps, opts) => leaf(LEAF_NAME3, {
26132
26211
  const promptWrote = await writeTextAtomic(String(paths.value.promptFile), String(prompt));
26133
26212
  if (!promptWrote.ok) return Result.error(promptWrote.error);
26134
26213
  const chainSessionId = currentSessionId();
26135
- const spawn3 = await deps.provider.generate({
26214
+ const spawn2 = await deps.provider.generate({
26136
26215
  prompt,
26137
26216
  cwd: paths.value.outputDir,
26138
26217
  additionalRoots: opts.additionalRoots,
@@ -26145,7 +26224,7 @@ var reviewRoundLeaf = (deps, opts) => leaf(LEAF_NAME3, {
26145
26224
  // the provider's SIGTERM ladder instead of letting it run to completion.
26146
26225
  ...signal !== void 0 ? { abortSignal: signal } : {}
26147
26226
  });
26148
- if (!spawn3.ok) return Result.error(spawn3.error);
26227
+ if (!spawn2.ok) return Result.error(spawn2.error);
26149
26228
  const validated = await validateSignalsFile(paths.value.outputDir, reviewRoundOutputContract);
26150
26229
  if (!validated.ok) return Result.error(validated.error);
26151
26230
  for (const sig of validated.value) deps.publishSignal(sig);
@@ -26375,7 +26454,7 @@ var stampPass = (collected, accepted, retired, stampTime, path2) => {
26375
26454
  var isLive = (record) => record.retiredAt === void 0 || record.retiredAt === null;
26376
26455
 
26377
26456
  // src/application/flows/_shared/memory/distill-propose.ts
26378
- import { promises as fs29 } from "fs";
26457
+ import { promises as fs28 } from "fs";
26379
26458
  import { join as join58 } from "path";
26380
26459
 
26381
26460
  // src/integration/ai/prompts/distill-learnings/definition.ts
@@ -26487,7 +26566,7 @@ var distillProposeUseCase = async (deps, tool, input, signal) => {
26487
26566
  if (!prompt.ok) return Result.error(prompt.error);
26488
26567
  const toolDir = join58(String(deps.distillRoot), tool);
26489
26568
  try {
26490
- await fs29.mkdir(toolDir, { recursive: true });
26569
+ await fs28.mkdir(toolDir, { recursive: true });
26491
26570
  } catch (cause) {
26492
26571
  return Result.error(
26493
26572
  new StorageError({
@@ -26541,11 +26620,11 @@ var renderProjectTooling = (repository) => {
26541
26620
  const lines = [];
26542
26621
  if (repository.setupScript !== void 0) lines.push(`- Setup: ${repository.setupScript}`);
26543
26622
  if (repository.verifyScript !== void 0) lines.push(`- Verify: ${repository.verifyScript}`);
26544
- return lines.length > 0 ? lines.join("\n") : "(none detected)";
26623
+ return renderProjectToolingSection(lines.length > 0 ? lines.join("\n") : void 0);
26545
26624
  };
26546
26625
  var safeReadText = async (path2) => {
26547
26626
  try {
26548
- return await fs29.readFile(path2, "utf8");
26627
+ return await fs28.readFile(path2, "utf8");
26549
26628
  } catch {
26550
26629
  return void 0;
26551
26630
  }
@@ -26608,7 +26687,7 @@ var distillConfirmLeaf = (deps, tool) => leaf(`distill-confirm-${tool}`, {
26608
26687
  });
26609
26688
 
26610
26689
  // src/application/flows/_shared/memory/distill-write.ts
26611
- import { promises as fs30 } from "fs";
26690
+ import { promises as fs29 } from "fs";
26612
26691
  var distillWriteUseCase = async (deps, tool, input) => {
26613
26692
  const log = deps.logger.named(`memory.distill-write-${tool}`);
26614
26693
  if (!input.accepted || input.proposedContent === void 0 || input.targetPath === void 0) {
@@ -26637,14 +26716,14 @@ var distillWriteUseCase = async (deps, tool, input) => {
26637
26716
  var makeBackupPath = (targetPath, now) => `${String(targetPath)}.bak.${String(now).replace(/:/g, "-")}`;
26638
26717
  var fileExists = async (path2) => {
26639
26718
  try {
26640
- return (await fs30.stat(path2)).isFile();
26719
+ return (await fs29.stat(path2)).isFile();
26641
26720
  } catch {
26642
26721
  return false;
26643
26722
  }
26644
26723
  };
26645
26724
  var safeReadText2 = async (path2) => {
26646
26725
  try {
26647
- return await fs30.readFile(path2, "utf8");
26726
+ return await fs29.readFile(path2, "utf8");
26648
26727
  } catch {
26649
26728
  return void 0;
26650
26729
  }
@@ -27801,7 +27880,7 @@ var probeReadinessLeaf = (deps, tool) => leaf(`probe-${tool}`, {
27801
27880
  });
27802
27881
 
27803
27882
  // src/application/flows/readiness/leaves/propose.ts
27804
- import { promises as fs31 } from "fs";
27883
+ import { promises as fs30 } from "fs";
27805
27884
 
27806
27885
  // src/integration/ai/prompts/readiness/definition.ts
27807
27886
  var CLAUDE_CODE = "claude-code";
@@ -28020,7 +28099,17 @@ var readinessExampleSignals = [
28020
28099
  content: "# Verify\n\nRun `<verify command>` (typecheck + lint + test).",
28021
28100
  timestamp: EXAMPLE_TS6
28022
28101
  },
28023
- { type: "skill-suggestions", names: ["typescript-strict", "code-style-conventions"], timestamp: EXAMPLE_TS6 }
28102
+ { type: "skill-suggestions", names: ["typescript-strict", "code-style-conventions"], timestamp: EXAMPLE_TS6 },
28103
+ {
28104
+ type: "learning",
28105
+ text: "The verify command lives in a workspace package script, not the repo root manifest.",
28106
+ timestamp: EXAMPLE_TS6
28107
+ },
28108
+ {
28109
+ type: "note",
28110
+ text: "No lint config found, so the proposed verify skill covers typecheck + test only.",
28111
+ timestamp: EXAMPLE_TS6
28112
+ }
28024
28113
  ];
28025
28114
  var readinessOutputContract = {
28026
28115
  schemaVersion: 1,
@@ -28060,7 +28149,7 @@ var proposeReadinessUseCase = async (deps, tool, input, abortSignal) => {
28060
28149
  let existingBody;
28061
28150
  if (existingPath !== void 0) {
28062
28151
  try {
28063
- existingBody = await fs31.readFile(existingPath, "utf8");
28152
+ existingBody = await fs30.readFile(existingPath, "utf8");
28064
28153
  } catch {
28065
28154
  existingBody = void 0;
28066
28155
  }
@@ -28192,7 +28281,7 @@ var proposeReadinessLeaf = (deps, tool) => leaf(`propose-${tool}`, {
28192
28281
  });
28193
28282
 
28194
28283
  // src/application/flows/readiness/leaves/write.ts
28195
- import { promises as fs32 } from "fs";
28284
+ import { promises as fs31 } from "fs";
28196
28285
  var writeReadinessUseCase = async (deps, tool, input) => {
28197
28286
  const log = deps.logger.named(`readiness.write-${tool}`);
28198
28287
  if (!input.accepted || input.proposal === void 0) {
@@ -28226,7 +28315,7 @@ var makeBackupPath2 = (targetPath, now) => {
28226
28315
  };
28227
28316
  var fileExists2 = async (path2) => {
28228
28317
  try {
28229
- const stat = await fs32.stat(path2);
28318
+ const stat = await fs31.stat(path2);
28230
28319
  return stat.isFile();
28231
28320
  } catch {
28232
28321
  return false;
@@ -28234,7 +28323,7 @@ var fileExists2 = async (path2) => {
28234
28323
  };
28235
28324
  var safeReadText3 = async (path2) => {
28236
28325
  try {
28237
- return await fs32.readFile(path2, "utf8");
28326
+ return await fs31.readFile(path2, "utf8");
28238
28327
  } catch {
28239
28328
  return void 0;
28240
28329
  }
@@ -28259,11 +28348,11 @@ var writeReadinessLeaf = (deps, tool) => leaf(`write-${tool}`, {
28259
28348
  });
28260
28349
 
28261
28350
  // src/application/flows/_shared/allocate-run-dir.ts
28262
- import { promises as fs34 } from "fs";
28351
+ import { promises as fs33 } from "fs";
28263
28352
  import { join as join63 } from "path";
28264
28353
 
28265
28354
  // src/integration/ai/runs/_engine/run-artifacts.ts
28266
- import { promises as fs33 } from "fs";
28355
+ import { promises as fs32 } from "fs";
28267
28356
  import { join as join62 } from "path";
28268
28357
  var BODY_PREVIEW_LIMIT = 800;
28269
28358
  var buildRunDirName = () => {
@@ -28274,7 +28363,7 @@ var buildRunDirName = () => {
28274
28363
  var readRunBodyPreview = async (runDir, options) => {
28275
28364
  let raw;
28276
28365
  try {
28277
- raw = await fs33.readFile(join62(String(runDir), "body.txt"), "utf8");
28366
+ raw = await fs32.readFile(join62(String(runDir), "body.txt"), "utf8");
28278
28367
  } catch (cause) {
28279
28368
  if (isErrnoException(cause) && cause.code === "ENOENT") return void 0;
28280
28369
  const code = isErrnoException(cause) ? cause.code : "unknown";
@@ -28294,7 +28383,7 @@ var allocateRunDirLeaf = (opts) => leaf(opts.name, {
28294
28383
  useCase: {
28295
28384
  execute: async (input) => {
28296
28385
  try {
28297
- await fs34.mkdir(input.path, { recursive: true });
28386
+ await fs33.mkdir(input.path, { recursive: true });
28298
28387
  } catch (cause) {
28299
28388
  return Result.error(
28300
28389
  new StorageError({
@@ -28682,16 +28771,16 @@ var proposeUseCase = async (deps, input, abortSignal) => {
28682
28771
  if (!prompt.ok) return Result.error(prompt.error);
28683
28772
  const promptWrote = await writeTextAtomic(String(paths.value.promptFile), String(prompt.value));
28684
28773
  if (!promptWrote.ok) return Result.error(promptWrote.error);
28685
- const spawn3 = await deps.provider.generate(
28774
+ const spawn2 = await deps.provider.generate(
28686
28775
  readOnlySignalsSession(buildSessionOpts(deps, input, paths.value, prompt.value, abortSignal))
28687
28776
  );
28688
- if (!spawn3.ok) {
28777
+ if (!spawn2.ok) {
28689
28778
  log.error(`provider failed for repo ${input.repository.name}`, {
28690
28779
  repositoryId: String(input.repository.id),
28691
- error: spawn3.error.message,
28780
+ error: spawn2.error.message,
28692
28781
  runDir: String(input.runDir)
28693
28782
  });
28694
- return Result.error(spawn3.error);
28783
+ return Result.error(spawn2.error);
28695
28784
  }
28696
28785
  const validated = await validateSignalsFile(input.runDir, detectSkillsOutputContract);
28697
28786
  if (!validated.ok) {
@@ -29127,16 +29216,16 @@ var proposeUseCase2 = async (deps, input, abortSignal) => {
29127
29216
  if (!prompt.ok) return Result.error(prompt.error);
29128
29217
  const promptWrote = await writeTextAtomic(String(paths.value.promptFile), String(prompt.value));
29129
29218
  if (!promptWrote.ok) return Result.error(promptWrote.error);
29130
- const spawn3 = await deps.provider.generate(
29219
+ const spawn2 = await deps.provider.generate(
29131
29220
  readOnlySignalsSession(buildSessionOpts2(deps, input, paths.value, prompt.value, abortSignal))
29132
29221
  );
29133
- if (!spawn3.ok) {
29222
+ if (!spawn2.ok) {
29134
29223
  log.error(`provider failed for repo ${input.repository.name}`, {
29135
29224
  repositoryId: String(input.repository.id),
29136
- error: spawn3.error.message,
29225
+ error: spawn2.error.message,
29137
29226
  runDir: String(input.runDir)
29138
29227
  });
29139
- return Result.error(spawn3.error);
29228
+ return Result.error(spawn2.error);
29140
29229
  }
29141
29230
  const validated = await validateSignalsFile(input.runDir, detectScriptsOutputContract);
29142
29231
  if (!validated.ok) {
@@ -29632,6 +29721,21 @@ var ideateExampleSignals = [
29632
29721
  type: "ideated-tickets",
29633
29722
  outputJson: '{"requirements":"# Export CSV\\n\\n## Problem\\n\u2026","tasks":[{"name":"Wire endpoint","projectPath":"/abs/repo","steps":["\u2026"],"verificationCriteria":["\u2026"]}]}',
29634
29723
  timestamp: EXAMPLE_TS9
29724
+ },
29725
+ {
29726
+ type: "decision",
29727
+ text: "Scoped export to CSV only for v1; XLSX deferred to a follow-up ticket.",
29728
+ timestamp: EXAMPLE_TS9
29729
+ },
29730
+ {
29731
+ type: "learning",
29732
+ text: "The repo runs its integration suite from a separate config; the root test command does not cover it.",
29733
+ timestamp: EXAMPLE_TS9
29734
+ },
29735
+ {
29736
+ type: "note",
29737
+ text: "The user ruled out changing the existing download endpoint, so the new route is additive.",
29738
+ timestamp: EXAMPLE_TS9
29635
29739
  }
29636
29740
  ];
29637
29741
  var ideateOutputContract = {
@@ -30943,8 +31047,8 @@ var defaultRowFor = (flowId, settings) => {
30943
31047
  return primaryFlowRow(settings.ai, aiFlow);
30944
31048
  };
30945
31049
  var labelKeepDefault = (value) => `Keep default (${value ?? "unset"})`;
30946
- var formatRow = (row, globalEffort) => {
30947
- const resolved = resolveEffortForRow(row, globalEffort);
31050
+ var formatRow = (row, globalEffort, flow) => {
31051
+ const resolved = resolveEffortForRow(row, globalEffort, flow);
30948
31052
  return `${row.provider} / ${row.model} / ${resolved ?? "auto"}`;
30949
31053
  };
30950
31054
  var pickProviderStep = async (interactive, header, defaultRow) => {
@@ -30978,9 +31082,9 @@ var computeEffortDefaultLabel = (defaultRow, globalEffort, providerChanged, mode
30978
31082
  }
30979
31083
  return labelKeepDefault(resolvedRowEffort ?? "auto");
30980
31084
  };
30981
- var pickEffortStep = async (interactive, header, defaultRow, globalEffort, effectiveProvider, providerChanged, modelChanged) => {
31085
+ var pickEffortStep = async (interactive, header, defaultRow, globalEffort, effectiveProvider, providerChanged, modelChanged, flow) => {
30982
31086
  const effortCatalog = PROVIDER_EFFORT_LEVELS[effectiveProvider];
30983
- const resolvedRowEffort = resolveEffortForRow(defaultRow, globalEffort);
31087
+ const resolvedRowEffort = resolveEffortForRow(defaultRow, globalEffort, flow);
30984
31088
  const effortDefaultLabel = computeEffortDefaultLabel(
30985
31089
  defaultRow,
30986
31090
  globalEffort,
@@ -31005,7 +31109,7 @@ var assembleRowOverride = (defaultRow, providerChanged, providerValue, modelValu
31005
31109
  if (providerChanged && override.model === void 0) override.model = modelValue;
31006
31110
  return override;
31007
31111
  };
31008
- var customizeRow = async (interactive, header, defaultRow, globalEffort, availableModelsFor) => {
31112
+ var customizeRow = async (interactive, header, defaultRow, globalEffort, availableModelsFor, flow) => {
31009
31113
  const providerStep = await pickProviderStep(interactive, header, defaultRow);
31010
31114
  if (providerStep === void 0) return void 0;
31011
31115
  const { providerValue, providerChanged, effectiveProvider } = providerStep;
@@ -31026,14 +31130,15 @@ var customizeRow = async (interactive, header, defaultRow, globalEffort, availab
31026
31130
  globalEffort,
31027
31131
  effectiveProvider,
31028
31132
  providerChanged,
31029
- modelChanged
31133
+ modelChanged,
31134
+ flow
31030
31135
  );
31031
31136
  if (effortValue === void 0) return void 0;
31032
31137
  return assembleRowOverride(defaultRow, providerChanged, providerValue, modelValue, effortValue);
31033
31138
  };
31034
- var buildPickerHeader = (flowId, flowTitle, settings) => flowId === "implement" ? `${flowTitle} \u2014 current defaults:
31035
- generator: ${formatRow(settings.ai.implement.generator, settings.ai.effort)}
31036
- evaluator: ${formatRow(settings.ai.implement.evaluator, settings.ai.effort)}` : `${flowTitle} \u2014 current default: ${formatRow(defaultRowFor(flowId, settings), settings.ai.effort)}`;
31139
+ var buildPickerHeader = (flowId, flowTitle, settings, aiFlow) => flowId === "implement" ? `${flowTitle} \u2014 current defaults:
31140
+ generator: ${formatRow(settings.ai.implement.generator, settings.ai.effort, aiFlow)}
31141
+ evaluator: ${formatRow(settings.ai.implement.evaluator, settings.ai.effort, aiFlow)}` : `${flowTitle} \u2014 current default: ${formatRow(defaultRowFor(flowId, settings), settings.ai.effort, aiFlow)}`;
31037
31142
  var originLabel = (origin) => {
31038
31143
  switch (origin) {
31039
31144
  case "bundled-default":
@@ -31091,7 +31196,7 @@ var runSkillsStep = async (interactive, flowTitle, skillCandidates) => {
31091
31196
  skills: { disabled, saveAsDefault: saveAns.value === "remember" }
31092
31197
  };
31093
31198
  };
31094
- var runImplementCustomize = async (interactive, header, flowTitle, settings, availableModelsFor, skillsStepInput) => {
31199
+ var runImplementCustomize = async (interactive, header, flowTitle, settings, availableModelsFor, skillsStepInput, aiFlow) => {
31095
31200
  const roles = ["generator", "evaluator"];
31096
31201
  const collected = {};
31097
31202
  for (const role of roles) {
@@ -31099,7 +31204,7 @@ var runImplementCustomize = async (interactive, header, flowTitle, settings, ava
31099
31204
  const roleHeader = `${header}
31100
31205
 
31101
31206
  Role: ${role}`;
31102
- const result = await customizeRow(interactive, roleHeader, row, settings.ai.effort, availableModelsFor);
31207
+ const result = await customizeRow(interactive, roleHeader, row, settings.ai.effort, availableModelsFor, aiFlow);
31103
31208
  if (result === void 0) return { kind: "cancel" };
31104
31209
  if (Object.keys(result).length > 0) collected[role] = result;
31105
31210
  }
@@ -31123,10 +31228,10 @@ Role: ${role}`;
31123
31228
  ...skills !== void 0 ? { skills } : {}
31124
31229
  };
31125
31230
  };
31126
- var runSingleRowCustomize = async (interactive, header, flowId, flowTitle, settings, availableModelsFor, skillsStepInput) => {
31231
+ var runSingleRowCustomize = async (interactive, header, flowId, flowTitle, settings, availableModelsFor, skillsStepInput, aiFlow) => {
31127
31232
  const defaultRow = defaultRowFor(flowId, settings);
31128
31233
  if (defaultRow === void 0) return { kind: "defaults" };
31129
- const override = await customizeRow(interactive, header, defaultRow, settings.ai.effort, availableModelsFor);
31234
+ const override = await customizeRow(interactive, header, defaultRow, settings.ai.effort, availableModelsFor, aiFlow);
31130
31235
  if (override === void 0) return { kind: "cancel" };
31131
31236
  const candidates = await effectiveSkillCandidates(
31132
31237
  skillsStepInput.candidates,
@@ -31152,7 +31257,7 @@ var runCustomizePicker = async ({
31152
31257
  }) => {
31153
31258
  const aiFlow = aiFlowIdForPicker(flowId);
31154
31259
  if (aiFlow === void 0) return { kind: "defaults" };
31155
- const header = buildPickerHeader(flowId, flowTitle, settings);
31260
+ const header = buildPickerHeader(flowId, flowTitle, settings, aiFlow);
31156
31261
  const action = await interactive.askChoice(
31157
31262
  `${header}
31158
31263
 
@@ -31170,9 +31275,18 @@ What would you like to do?`,
31170
31275
  ...rebuildSkillCandidates !== void 0 ? { rebuild: rebuildSkillCandidates } : {}
31171
31276
  };
31172
31277
  if (flowId === "implement") {
31173
- return runImplementCustomize(interactive, header, flowTitle, settings, availableModelsFor, skillsStepInput);
31278
+ return runImplementCustomize(interactive, header, flowTitle, settings, availableModelsFor, skillsStepInput, aiFlow);
31174
31279
  }
31175
- return runSingleRowCustomize(interactive, header, flowId, flowTitle, settings, availableModelsFor, skillsStepInput);
31280
+ return runSingleRowCustomize(
31281
+ interactive,
31282
+ header,
31283
+ flowId,
31284
+ flowTitle,
31285
+ settings,
31286
+ availableModelsFor,
31287
+ skillsStepInput,
31288
+ aiFlow
31289
+ );
31176
31290
  };
31177
31291
 
31178
31292
  // src/application/flows/settings-set/flow.ts
@@ -31683,10 +31797,10 @@ var FeedbackLine = ({ text }) => {
31683
31797
  if (text === void 0) return null;
31684
31798
  if (typeof text === "string") {
31685
31799
  const { color: color2, body } = resolveStructured(text);
31686
- return /* @__PURE__ */ jsx45(Box34, { paddingX: spacing.indent, marginTop: 1, children: /* @__PURE__ */ jsx45(Text35, { color: color2, children: body }) });
31800
+ return /* @__PURE__ */ jsx45(Box34, { paddingX: spacing.indent, marginTop: spacing.section, children: /* @__PURE__ */ jsx45(Text35, { color: color2, children: body }) });
31687
31801
  }
31688
31802
  const { glyph, color } = toneConfig(text.tone);
31689
- return /* @__PURE__ */ jsx45(Box34, { paddingX: spacing.indent, marginTop: 1, children: /* @__PURE__ */ jsxs34(Text35, { color, children: [
31803
+ return /* @__PURE__ */ jsx45(Box34, { paddingX: spacing.indent, marginTop: spacing.section, children: /* @__PURE__ */ jsxs34(Text35, { color, children: [
31690
31804
  glyph.length > 0 ? `${glyph} ` : "",
31691
31805
  text.text
31692
31806
  ] }) });
@@ -34292,7 +34406,7 @@ var HeaderCardImpl = ({
34292
34406
  String(tasksTotal)
34293
34407
  ] })
34294
34408
  ] }),
34295
- isRunning && /* @__PURE__ */ jsx62(Box50, { marginLeft: 2, children: /* @__PURE__ */ jsx62(Spinner, { active: isRunning, color: inkColors.info, label: "live" }) })
34409
+ isRunning && /* @__PURE__ */ jsx62(Box50, { marginLeft: spacing.indent, children: /* @__PURE__ */ jsx62(Spinner, { active: isRunning, color: inkColors.info, label: "live" }) })
34296
34410
  ] }),
34297
34411
  /* @__PURE__ */ jsx62(
34298
34412
  ModelLines,
@@ -35062,7 +35176,7 @@ var TASK_STATUS_COLOR = {
35062
35176
  aborted: inkColors.warning,
35063
35177
  skipped: inkColors.muted
35064
35178
  };
35065
- var SidebarDivider = ({ width }) => /* @__PURE__ */ jsx69(Box56, { paddingX: 1, marginTop: spacing.gutter, children: /* @__PURE__ */ jsx69(Text56, { color: inkColors.rule, children: glyphs.sectionRule.repeat(Math.max(0, width - 2)) }) });
35179
+ var SidebarDivider = ({ width }) => /* @__PURE__ */ jsx69(Box56, { paddingX: spacing.gutter, marginTop: spacing.gutter, children: /* @__PURE__ */ jsx69(Text56, { color: inkColors.rule, children: glyphs.sectionRule.repeat(Math.max(0, width - 2)) }) });
35066
35180
  var truncateName = (name, maxChars) => name.length > maxChars ? `${name.slice(0, Math.max(0, maxChars - 1))}${glyphs.clipEllipsis}` : name;
35067
35181
  var TaskNavList = ({
35068
35182
  tasks,
@@ -39306,10 +39420,10 @@ var projectInput = (ctx) => {
39306
39420
  };
39307
39421
  var runAuthoringSpawn = async (deps, session, unitRoot, log) => {
39308
39422
  try {
39309
- const spawn3 = await deps.provider.generate(session);
39310
- if (!spawn3.ok) {
39311
- if (spawn3.error.code === ErrorCode.Aborted) return Result.error(spawn3.error);
39312
- log.warn(`create-pr: AI authoring failed, falling back to template (${spawn3.error.message})`);
39423
+ const spawn2 = await deps.provider.generate(session);
39424
+ if (!spawn2.ok) {
39425
+ if (spawn2.error.code === ErrorCode.Aborted) return Result.error(spawn2.error);
39426
+ log.warn(`create-pr: AI authoring failed, falling back to template (${spawn2.error.message})`);
39313
39427
  return Result.ok({});
39314
39428
  }
39315
39429
  const validated = await validateSignalsFile(unitRoot, generatePrContentOutputContract);
@@ -39779,7 +39893,7 @@ import { basename as basename5, join as join69 } from "path";
39779
39893
 
39780
39894
  // src/application/ui/tui/prompts/path-picker-prompt.tsx
39781
39895
  import { useEffect as useEffect40, useState as useState45 } from "react";
39782
- import { promises as fs35 } from "fs";
39896
+ import { promises as fs34 } from "fs";
39783
39897
  import { dirname as dirname21, join as join68 } from "path";
39784
39898
  import { homedir as homedir2 } from "os";
39785
39899
  import { Box as Box76, Text as Text72, useInput as useInput28 } from "ink";
@@ -39798,7 +39912,7 @@ var useDirectoryEntries = (cwd, showHidden) => {
39798
39912
  let cancelled = false;
39799
39913
  const load = async () => {
39800
39914
  try {
39801
- const items = await fs35.readdir(cwd, { withFileTypes: true });
39915
+ const items = await fs34.readdir(cwd, { withFileTypes: true });
39802
39916
  if (cancelled) return;
39803
39917
  const filtered = items.filter((d) => showHidden || !d.name.startsWith(".")).filter((d) => d.isDirectory()).map((d) => ({ name: d.name, isDirectory: true })).sort((a, b) => a.name.localeCompare(b.name));
39804
39918
  setEntries(filtered);
@@ -39880,7 +39994,7 @@ var submitTypedPath = async (raw, { setError, setTyping, onSubmit }) => {
39880
39994
  return;
39881
39995
  }
39882
39996
  try {
39883
- const stat = await fs35.stat(expanded);
39997
+ const stat = await fs34.stat(expanded);
39884
39998
  if (!stat.isDirectory()) {
39885
39999
  setError(`${expanded} is not a directory`);
39886
40000
  setTyping(false);
@@ -39955,8 +40069,8 @@ var PathPickerPrompt = ({
39955
40069
  ] }) }),
39956
40070
  /* @__PURE__ */ jsx94(Box76, { paddingX: spacing.indent, children: /* @__PURE__ */ jsx94(Text72, { dimColor: true, children: cwd }) }),
39957
40071
  error !== void 0 && /* @__PURE__ */ jsx94(Box76, { paddingX: spacing.indent, children: /* @__PURE__ */ jsx94(Text72, { color: inkColors.error, children: error }) }),
39958
- /* @__PURE__ */ jsx94(Box76, { flexDirection: "column", marginTop: 1, children: /* @__PURE__ */ jsx94(PathPickerRows, { rows, start, end, cursor }) }),
39959
- typing ? /* @__PURE__ */ jsxs72(Box76, { flexDirection: "column", marginTop: 1, paddingX: spacing.indent, children: [
40072
+ /* @__PURE__ */ jsx94(Box76, { flexDirection: "column", marginTop: spacing.section, children: /* @__PURE__ */ jsx94(PathPickerRows, { rows, start, end, cursor }) }),
40073
+ typing ? /* @__PURE__ */ jsxs72(Box76, { flexDirection: "column", marginTop: spacing.section, paddingX: spacing.indent, children: [
39960
40074
  /* @__PURE__ */ jsx94(Text72, { dimColor: true, children: "Type an absolute path (~/ allowed). Enter validates; esc returns to the picker." }),
39961
40075
  /* @__PURE__ */ jsx94(
39962
40076
  TextPrompt,
@@ -39967,7 +40081,7 @@ var PathPickerPrompt = ({
39967
40081
  onCancel: () => setTyping(false)
39968
40082
  }
39969
40083
  )
39970
- ] }) : /* @__PURE__ */ jsx94(Box76, { paddingX: spacing.indent, marginTop: 1, children: /* @__PURE__ */ jsxs72(Text72, { dimColor: true, children: [
40084
+ ] }) : /* @__PURE__ */ jsx94(Box76, { paddingX: spacing.indent, marginTop: spacing.section, children: /* @__PURE__ */ jsxs72(Text72, { dimColor: true, children: [
39971
40085
  "\u21B5 open/select \xB7 \u232B up \xB7 esc cancel \xB7 ~ home \xB7 t type \xB7 . ",
39972
40086
  showHidden ? "hide" : "show",
39973
40087
  " hidden"
@@ -40885,7 +40999,7 @@ var PickProjectView = () => {
40885
40999
  };
40886
41000
 
40887
41001
  // src/application/ui/tui/views/pick-sprint-view.tsx
40888
- import { useEffect as useEffect45, useMemo as useMemo29, useState as useState50 } from "react";
41002
+ import { useMemo as useMemo29, useState as useState50 } from "react";
40889
41003
  import { Box as Box85, Text as Text80, useInput as useInput31 } from "ink";
40890
41004
 
40891
41005
  // src/application/ui/tui/views/pick-sprint-internals/types.ts
@@ -40950,99 +41064,75 @@ var flatten = (groups, includeCreate) => {
40950
41064
  }
40951
41065
  return rows;
40952
41066
  };
40953
- var cursorableRowIndices = (rows) => {
40954
- const indices = [];
40955
- for (let i = 0; i < rows.length; i += 1) {
40956
- const kind = rows[i]?.kind;
40957
- if (kind === "sprint" || kind === "create") indices.push(i);
40958
- }
40959
- return indices;
40960
- };
40961
- var nextCursorableIndex = (rows, from, direction) => {
40962
- const candidates = cursorableRowIndices(rows);
40963
- if (candidates.length === 0) return from;
40964
- if (direction === 1) {
40965
- const next = candidates.find((i) => i > from);
40966
- return next ?? from;
40967
- }
40968
- let prev = from;
40969
- for (const i of candidates) {
40970
- if (i < from) prev = i;
40971
- else break;
40972
- }
40973
- return prev === from && candidates.includes(from) ? from : prev;
40974
- };
40975
- var cursorableNear = (rows, target, direction) => {
40976
- const candidates = cursorableRowIndices(rows);
40977
- if (candidates.length === 0) return target;
40978
- if (candidates.includes(target)) return target;
40979
- const below = candidates.find((i) => i > target);
40980
- let above;
40981
- for (const i of candidates) {
40982
- if (i < target) above = i;
40983
- else break;
41067
+ var CREATE_ROW_ID = "__create__";
41068
+ var cursorableRows = (rows) => rows.filter((r) => r.kind !== "header");
41069
+ var cursorableRowId = (row) => row.kind === "create" ? CREATE_ROW_ID : row.sprint.id;
41070
+ var preferredCursorId = (rows, preferredSprintId) => {
41071
+ if (preferredSprintId !== void 0) {
41072
+ const match = rows.find((r) => r.kind === "sprint" && r.sprint.id === preferredSprintId);
41073
+ if (match !== void 0) return match.sprint.id;
40984
41074
  }
40985
- const preferred = direction === 1 ? below : above;
40986
- const fallback = direction === 1 ? above : below;
40987
- return preferred ?? fallback ?? target;
41075
+ const firstSprint = rows.find((r) => r.kind === "sprint");
41076
+ if (firstSprint !== void 0) return firstSprint.sprint.id;
41077
+ const firstCreate = rows.find((r) => r.kind === "create");
41078
+ return firstCreate !== void 0 ? CREATE_ROW_ID : "";
40988
41079
  };
40989
41080
 
40990
41081
  // src/application/ui/tui/views/pick-sprint-internals/window.ts
40991
41082
  var VERTICAL_CHROME_ROWS = 12;
40992
41083
  var MIN_VISIBLE_ROWS = 8;
40993
- var computeWindow = (totalRows, cursor, visible) => {
40994
- if (totalRows <= visible) return { start: 0, end: totalRows, hiddenAbove: 0, hiddenBelow: 0 };
40995
- const half = Math.floor(visible / 2);
40996
- let start = Math.max(0, cursor - half);
40997
- let end = start + visible;
40998
- if (end > totalRows) {
40999
- end = totalRows;
41000
- start = Math.max(0, end - visible);
41001
- }
41002
- return { start, end, hiddenAbove: start, hiddenBelow: totalRows - end };
41003
- };
41004
41084
 
41005
41085
  // src/application/ui/tui/views/pick-sprint-internals/row-views.tsx
41006
41086
  import { useMemo as useMemo28 } from "react";
41007
41087
  import { Box as Box84, Text as Text79 } from "ink";
41008
41088
  import { jsx as jsx102, jsxs as jsxs80 } from "react/jsx-runtime";
41009
- var RowWindowView = ({
41089
+ var PickerRowList = ({
41010
41090
  rows,
41011
- cursor,
41012
41091
  visibleRows,
41013
- currentSprintId
41092
+ active,
41093
+ initialCursorId,
41094
+ currentSprintId,
41095
+ onSubmit
41014
41096
  }) => {
41015
- const window = useMemo28(() => computeWindow(rows.length, cursor, visibleRows), [rows.length, cursor, visibleRows]);
41016
- const slice = rows.slice(window.start, window.end);
41097
+ const items = useMemo28(() => cursorableRows(rows), [rows]);
41098
+ const { focusedItem } = useListWindow({
41099
+ items,
41100
+ getId: cursorableRowId,
41101
+ visibleRows,
41102
+ active,
41103
+ initialCursorId,
41104
+ onSubmit
41105
+ });
41106
+ const focusedId = focusedItem !== void 0 ? cursorableRowId(focusedItem) : void 0;
41107
+ const focusedRowIndex = useMemo28(() => {
41108
+ if (focusedId === void 0) return 0;
41109
+ const idx = rows.findIndex((row) => row.kind !== "header" && cursorableRowId(row) === focusedId);
41110
+ return idx < 0 ? 0 : idx;
41111
+ }, [rows, focusedId]);
41112
+ const renderWindow = useMemo28(
41113
+ () => computeListWindow(rows.length, focusedRowIndex, visibleRows),
41114
+ [rows.length, focusedRowIndex, visibleRows]
41115
+ );
41116
+ const renderRows = useMemo28(
41117
+ () => rows.slice(renderWindow.start, renderWindow.end),
41118
+ [rows, renderWindow.start, renderWindow.end]
41119
+ );
41017
41120
  return /* @__PURE__ */ jsxs80(Box84, { flexDirection: "column", children: [
41018
- window.hiddenAbove > 0 && /* @__PURE__ */ jsx102(Box84, { paddingX: spacing.indent, children: /* @__PURE__ */ jsxs80(Text79, { dimColor: true, children: [
41019
- "\u25B2 ",
41020
- String(window.hiddenAbove),
41021
- " more above"
41022
- ] }) }),
41023
- slice.map((row, i) => {
41024
- const absoluteIndex = i + window.start;
41025
- if (row.kind === "header") {
41026
- return /* @__PURE__ */ jsx102(HeaderRowView, { row }, `h-${row.groupKey}-${String(absoluteIndex)}`);
41027
- }
41028
- if (row.kind === "create") {
41029
- return /* @__PURE__ */ jsx102(CreateRowView, { focused: absoluteIndex === cursor }, `create-${String(absoluteIndex)}`);
41030
- }
41121
+ /* @__PURE__ */ jsx102(OverflowRow, { direction: "above", count: renderWindow.start }),
41122
+ renderRows.map((row) => {
41123
+ if (row.kind === "header") return /* @__PURE__ */ jsx102(HeaderRowView, { row }, `h-${row.groupKey}`);
41124
+ if (row.kind === "create") return /* @__PURE__ */ jsx102(CreateRowView, { focused: focusedId === cursorableRowId(row) }, "create");
41031
41125
  return /* @__PURE__ */ jsx102(
41032
41126
  SprintRowView,
41033
41127
  {
41034
41128
  sprint: row.sprint,
41035
- focused: absoluteIndex === cursor,
41129
+ focused: focusedId === row.sprint.id,
41036
41130
  isCurrent: currentSprintId === row.sprint.id
41037
41131
  },
41038
41132
  row.sprint.id
41039
41133
  );
41040
41134
  }),
41041
- window.hiddenBelow > 0 && /* @__PURE__ */ jsx102(Box84, { paddingX: spacing.indent, children: /* @__PURE__ */ jsxs80(Text79, { dimColor: true, children: [
41042
- "\u25BC ",
41043
- String(window.hiddenBelow),
41044
- " more below"
41045
- ] }) })
41135
+ /* @__PURE__ */ jsx102(OverflowRow, { direction: "below", count: rows.length - renderWindow.end })
41046
41136
  ] });
41047
41137
  };
41048
41138
  var CreateRowView = ({ focused }) => /* @__PURE__ */ jsxs80(Box84, { flexDirection: "column", paddingX: spacing.indent, children: [
@@ -41104,15 +41194,6 @@ var SprintRowView = ({
41104
41194
 
41105
41195
  // src/application/ui/tui/views/pick-sprint-view.tsx
41106
41196
  import { Fragment as Fragment12, jsx as jsx103, jsxs as jsxs81 } from "react/jsx-runtime";
41107
- var preferredCursorIndex = (rows, preferredSprintId) => {
41108
- if (preferredSprintId !== void 0) {
41109
- const i = rows.findIndex((r) => r.kind === "sprint" && r.sprint.id === preferredSprintId);
41110
- if (i !== -1) return i;
41111
- }
41112
- const firstSprint = rows.findIndex((r) => r.kind === "sprint");
41113
- if (firstSprint !== -1) return firstSprint;
41114
- return cursorableRowIndices(rows)[0] ?? 0;
41115
- };
41116
41197
  var usePickerRows = (deps, selection) => {
41117
41198
  const [scopeAll, setScopeAll] = useState50(true);
41118
41199
  const [hideDone, setHideDone] = useState50(false);
@@ -41147,28 +41228,7 @@ var usePickerRows = (deps, selection) => {
41147
41228
  if (!hideDone || sprintCount > 0) return false;
41148
41229
  return flatten(buildGroups(data, selection.projectId, scopeAll), false).some((r) => r.kind === "sprint");
41149
41230
  }, [hideDone, sprintCount, data, selection.projectId, scopeAll]);
41150
- const initialIdx = useMemo29(() => preferredCursorIndex(rows, selection.sprintId), [rows, selection.sprintId]);
41151
- const [cursor, setCursor] = useState50(initialIdx);
41152
- useEffect45(() => {
41153
- setCursor((c) => {
41154
- if (rows.length === 0) return 0;
41155
- if (c >= rows.length) return Math.max(0, rows.length - 1);
41156
- const focused = rows[c];
41157
- if (focused?.kind !== "sprint" && focused?.kind !== "create") {
41158
- return cursorableRowIndices(rows)[0] ?? c;
41159
- }
41160
- return c;
41161
- });
41162
- }, [rows]);
41163
- useEffect45(() => {
41164
- setCursor(initialIdx);
41165
- }, [initialIdx]);
41166
- const toggleScope = () => {
41167
- const next = !scopeAll;
41168
- setScopeAll(next);
41169
- const nextRows = flatten(buildGroups(visibleData, selection.projectId, next), includeCreate);
41170
- setCursor(preferredCursorIndex(nextRows, selection.sprintId));
41171
- };
41231
+ const toggleScope = () => setScopeAll((v) => !v);
41172
41232
  return {
41173
41233
  state,
41174
41234
  reload,
@@ -41179,24 +41239,9 @@ var usePickerRows = (deps, selection) => {
41179
41239
  scopeAll,
41180
41240
  hideDone,
41181
41241
  setHideDone,
41182
- toggleScope,
41183
- cursor,
41184
- setCursor
41242
+ toggleScope
41185
41243
  };
41186
41244
  };
41187
- var isNavigationInput = (input, key) => key.upArrow || key.downArrow || key.pageUp || key.pageDown || key.home || key.end || input === "j" || input === "k";
41188
- var resolveArrowMove = (rows, cursor, visibleRows, input, key) => {
41189
- if (key.upArrow || input === "k") return nextCursorableIndex(rows, cursor, -1);
41190
- if (key.downArrow || input === "j") return nextCursorableIndex(rows, cursor, 1);
41191
- if (key.pageUp) return cursorableNear(rows, Math.max(0, cursor - visibleRows), -1);
41192
- if (key.pageDown) return cursorableNear(rows, Math.min(rows.length - 1, cursor + visibleRows), 1);
41193
- if (key.home) return cursorableRowIndices(rows)[0] ?? 0;
41194
- if (key.end) {
41195
- const candidates = cursorableRowIndices(rows);
41196
- return candidates[candidates.length - 1] ?? cursor;
41197
- }
41198
- return void 0;
41199
- };
41200
41245
  var PickerBody = ({
41201
41246
  helpOpen,
41202
41247
  state,
@@ -41205,11 +41250,14 @@ var PickerBody = ({
41205
41250
  scopeAll,
41206
41251
  hideDone,
41207
41252
  rows,
41208
- cursor,
41209
41253
  visibleRows,
41254
+ listActive,
41255
+ initialCursorId,
41256
+ listKey,
41210
41257
  currentSprintId,
41211
41258
  currentSprintLabel,
41212
- feedback: feedback2
41259
+ feedback: feedback2,
41260
+ onRowSubmit
41213
41261
  }) => helpOpen ? /* @__PURE__ */ jsx103(HelpOverlay, {}) : state.kind === "loading" || state.kind === "idle" ? /* @__PURE__ */ jsx103(LoadingRow, { label: "Loading sprints\u2026" }) : state.kind === "error" ? /* @__PURE__ */ jsx103(LoadErrorRow, { message: "Failed to load sprints.", color: inkColors.error }) : sprintCount === 0 ? /* @__PURE__ */ jsx103(Box85, { flexDirection: "column", paddingX: spacing.indent, children: hiddenByDoneFilter ? /* @__PURE__ */ jsxs81(Fragment12, { children: [
41214
41262
  /* @__PURE__ */ jsx103(Text80, { children: "All sprints here are done (hidden)." }),
41215
41263
  /* @__PURE__ */ jsx103(Text80, { dimColor: true, children: "Press f to show them, or + to create a new one." })
@@ -41234,7 +41282,18 @@ var PickerBody = ({
41234
41282
  /* @__PURE__ */ jsx103(Text80, { color: inkColors.primary, bold: true, children: currentSprintLabel })
41235
41283
  ] }) : /* @__PURE__ */ jsx103(Text80, { children: "press \u21B5 to confirm" })
41236
41284
  ] }) }),
41237
- /* @__PURE__ */ jsx103(RowWindowView, { rows, cursor, visibleRows, currentSprintId }),
41285
+ /* @__PURE__ */ jsx103(
41286
+ PickerRowList,
41287
+ {
41288
+ rows,
41289
+ visibleRows,
41290
+ active: listActive,
41291
+ initialCursorId,
41292
+ currentSprintId,
41293
+ onSubmit: onRowSubmit
41294
+ },
41295
+ listKey
41296
+ ),
41238
41297
  /* @__PURE__ */ jsx103(Box85, { marginTop: spacing.section, paddingX: spacing.indent, children: /* @__PURE__ */ jsxs81(Text80, { dimColor: true, children: [
41239
41298
  glyphs.bullet,
41240
41299
  " \u21B5 use the highlighted sprint ",
@@ -41248,67 +41307,15 @@ var PickerBody = ({
41248
41307
  glyphs.bullet,
41249
41308
  " + create a new one"
41250
41309
  ] }) }),
41251
- feedback2 !== void 0 && /* @__PURE__ */ jsx103(Box85, { paddingX: spacing.indent, marginTop: 1, children: /* @__PURE__ */ jsx103(Text80, { color: inkColors.error, children: feedback2 }) })
41310
+ feedback2 !== void 0 && /* @__PURE__ */ jsx103(Box85, { paddingX: spacing.indent, marginTop: spacing.section, children: /* @__PURE__ */ jsx103(Text80, { color: inkColors.error, children: feedback2 }) })
41252
41311
  ] });
41253
- var usePickerInput = ({
41254
- modalOpen,
41255
- rows,
41256
- cursor,
41257
- visibleRows,
41258
- setCursor,
41259
- pick,
41260
- launchCreateSprint: launchCreateSprint2,
41261
- toggleScope,
41262
- setHideDone,
41263
- reload
41264
- }) => {
41265
- useInput31((input, key) => {
41266
- if (modalOpen) return;
41267
- if (isNavigationInput(input, key)) {
41268
- setCursor((c) => resolveArrowMove(rows, c, visibleRows, input, key) ?? c);
41269
- return;
41270
- }
41271
- if (key.return) {
41272
- const row = rows[cursor];
41273
- if (row?.kind === "sprint") pick(row.sprint);
41274
- else if (row?.kind === "create") void launchCreateSprint2();
41275
- return;
41276
- }
41277
- if (input === "t") {
41278
- toggleScope();
41279
- return;
41280
- }
41281
- if (input === "f") {
41282
- setHideDone((v) => !v);
41283
- return;
41284
- }
41285
- if (input === "+" || input === "c") {
41286
- void launchCreateSprint2();
41287
- return;
41288
- }
41289
- if (input === "r") reload();
41290
- });
41291
- };
41292
41312
  var PickSprintView = () => {
41293
41313
  const deps = useDeps();
41294
41314
  const router = useRouter();
41295
41315
  const selection = useSelection();
41296
41316
  const ui = useUiState();
41297
41317
  const [feedback2, setFeedback] = useState50(void 0);
41298
- const {
41299
- state,
41300
- reload,
41301
- data,
41302
- rows,
41303
- sprintCount,
41304
- hiddenByDoneFilter,
41305
- scopeAll,
41306
- hideDone,
41307
- setHideDone,
41308
- toggleScope,
41309
- cursor,
41310
- setCursor
41311
- } = usePickerRows(deps, selection);
41318
+ const { state, reload, data, rows, sprintCount, hiddenByDoneFilter, scopeAll, hideDone, setHideDone, toggleScope } = usePickerRows(deps, selection);
41312
41319
  useViewHints([
41313
41320
  { keys: "\u2191/\u2193/j/k", label: "move" },
41314
41321
  { keys: "\u21B5", label: "use sprint" },
@@ -41335,18 +41342,28 @@ var PickSprintView = () => {
41335
41342
  onError: setFeedback,
41336
41343
  noProjectMessage: `${glyphs.cross} select a project first`
41337
41344
  });
41338
- usePickerInput({
41339
- modalOpen: ui.modalOpen,
41340
- rows,
41341
- cursor,
41342
- visibleRows,
41343
- setCursor,
41344
- pick,
41345
- launchCreateSprint: launchCreateSprint2,
41346
- toggleScope,
41347
- setHideDone,
41348
- reload
41345
+ const onRowSubmit = (row) => {
41346
+ if (row.kind === "sprint") pick(row.sprint);
41347
+ else void launchCreateSprint2();
41348
+ };
41349
+ useInput31((input) => {
41350
+ if (ui.modalOpen) return;
41351
+ if (input === "t") {
41352
+ toggleScope();
41353
+ return;
41354
+ }
41355
+ if (input === "f") {
41356
+ setHideDone((v) => !v);
41357
+ return;
41358
+ }
41359
+ if (input === "+" || input === "c") {
41360
+ void launchCreateSprint2();
41361
+ return;
41362
+ }
41363
+ if (input === "r") reload();
41349
41364
  });
41365
+ const initialCursorId = useMemo29(() => preferredCursorId(rows, selection.sprintId), [rows, selection.sprintId]);
41366
+ const listKey = `${String(scopeAll)}-${selection.sprintId ?? "unseeded"}`;
41350
41367
  return /* @__PURE__ */ jsx103(ViewShell, { title: "Pick a sprint", subtitle: "Switch sprint (and project) in one step", suppressScrollArrows: true, children: /* @__PURE__ */ jsx103(
41351
41368
  PickerBody,
41352
41369
  {
@@ -41357,11 +41374,14 @@ var PickSprintView = () => {
41357
41374
  scopeAll,
41358
41375
  hideDone,
41359
41376
  rows,
41360
- cursor,
41361
41377
  visibleRows,
41378
+ listActive: !ui.modalOpen,
41379
+ initialCursorId,
41380
+ listKey,
41362
41381
  currentSprintId: selection.sprintId,
41363
41382
  currentSprintLabel: selection.sprintLabel,
41364
- feedback: feedback2
41383
+ feedback: feedback2,
41384
+ onRowSubmit
41365
41385
  }
41366
41386
  ) });
41367
41387
  };
@@ -41416,11 +41436,10 @@ var renderView = (entry) => {
41416
41436
  };
41417
41437
 
41418
41438
  // src/application/ui/tui/runtime/use-global-keys.ts
41419
- import { useEffect as useEffect46, useMemo as useMemo30, useRef as useRef18 } from "react";
41439
+ import { useEffect as useEffect45, useMemo as useMemo30, useRef as useRef18 } from "react";
41420
41440
  import { useApp, useInput as useInput32 } from "ink";
41421
41441
 
41422
41442
  // src/integration/io/clipboard.ts
41423
- import { spawn as nodeSpawn2 } from "child_process";
41424
41443
  var resolveHelpers = ({ platform, env }) => {
41425
41444
  if (platform === "darwin") return [{ cmd: "pbcopy", args: [] }];
41426
41445
  if (platform === "win32") return [{ cmd: "clip.exe", args: [] }];
@@ -41439,10 +41458,10 @@ var resolveHelpers = ({ platform, env }) => {
41439
41458
  return [];
41440
41459
  };
41441
41460
  var SPAWN_FAILED = "spawn-failed";
41442
- var runHelper = (spawn3, helper, text) => new Promise((resolve) => {
41461
+ var runHelper = (spawn2, helper, text) => new Promise((resolve) => {
41443
41462
  let child;
41444
41463
  try {
41445
- child = spawn3(helper.cmd, helper.args, { stdio: ["pipe", "pipe", "pipe"] });
41464
+ child = spawn2(helper.cmd, helper.args, { stdio: ["pipe", "pipe", "pipe"] });
41446
41465
  } catch (cause) {
41447
41466
  resolve(
41448
41467
  Result.error({
@@ -41486,7 +41505,7 @@ var runHelper = (spawn3, helper, text) => new Promise((resolve) => {
41486
41505
  }
41487
41506
  });
41488
41507
  var createCopyToClipboard = (opts = {}) => {
41489
- const spawn3 = opts.spawn ?? nodeSpawn2;
41508
+ const spawn2 = opts.spawn ?? crossPlatformSpawn;
41490
41509
  const platform = opts.platform ?? process.platform;
41491
41510
  const env = opts.env ?? process.env;
41492
41511
  const helpers = resolveHelpers({ platform, env });
@@ -41499,7 +41518,7 @@ var createCopyToClipboard = (opts = {}) => {
41499
41518
  }
41500
41519
  let lastError;
41501
41520
  for (const helper of helpers) {
41502
- const result = await runHelper(spawn3, helper, text);
41521
+ const result = await runHelper(spawn2, helper, text);
41503
41522
  if (result.ok) return result;
41504
41523
  lastError = result.error;
41505
41524
  if (result.error.code !== "no-helper") return result;
@@ -41528,7 +41547,7 @@ var useGlobalKeys = (opts = {}) => {
41528
41547
  [opts.copyToClipboard]
41529
41548
  );
41530
41549
  const clearTimerRef = useRef18(void 0);
41531
- useEffect46(
41550
+ useEffect45(
41532
41551
  () => () => {
41533
41552
  if (clearTimerRef.current !== void 0) clearTimeout(clearTimerRef.current);
41534
41553
  },
@@ -41697,7 +41716,7 @@ var scheduleClear = (eventBus, ref3) => {
41697
41716
  };
41698
41717
 
41699
41718
  // src/application/ui/tui/components/memory-pressure-banner.tsx
41700
- import { useEffect as useEffect47, useState as useState51 } from "react";
41719
+ import { useEffect as useEffect46, useState as useState51 } from "react";
41701
41720
  import { Box as Box86, Text as Text81 } from "ink";
41702
41721
  import { jsxs as jsxs82 } from "react/jsx-runtime";
41703
41722
  var formatMb = (bytes) => `${(bytes / 1048576).toFixed(0)} MB`;
@@ -41705,7 +41724,7 @@ var formatPercent = (ratio) => `${Math.round(ratio * 100)}%`;
41705
41724
  var MemoryPressureBanner = () => {
41706
41725
  const deps = useDeps();
41707
41726
  const [latest, setLatest] = useState51(void 0);
41708
- useEffect47(() => {
41727
+ useEffect46(() => {
41709
41728
  return deps.eventBus.subscribe((event) => {
41710
41729
  if (event.type === "memory-pressure") setLatest(event);
41711
41730
  });
@@ -41728,13 +41747,13 @@ var MemoryPressureBanner = () => {
41728
41747
  };
41729
41748
 
41730
41749
  // src/application/ui/tui/components/chain-log-degraded-banner.tsx
41731
- import { useEffect as useEffect48, useState as useState52 } from "react";
41750
+ import { useEffect as useEffect47, useState as useState52 } from "react";
41732
41751
  import { Box as Box87, Text as Text82 } from "ink";
41733
41752
  import { jsx as jsx105, jsxs as jsxs83 } from "react/jsx-runtime";
41734
41753
  var ChainLogDegradedBanner = () => {
41735
41754
  const deps = useDeps();
41736
41755
  const [degraded, setDegraded] = useState52(false);
41737
- useEffect48(() => {
41756
+ useEffect47(() => {
41738
41757
  return deps.eventBus.subscribe((event) => {
41739
41758
  if (event.type === "chain-log-degraded") setDegraded(true);
41740
41759
  });
@@ -41747,9 +41766,9 @@ var ChainLogDegradedBanner = () => {
41747
41766
  };
41748
41767
 
41749
41768
  // src/application/ui/tui/components/progress-overlay.tsx
41750
- import { promises as fs36 } from "fs";
41769
+ import { promises as fs35 } from "fs";
41751
41770
  import { join as join71 } from "path";
41752
- import { useEffect as useEffect49, useState as useState53 } from "react";
41771
+ import { useEffect as useEffect48, useState as useState53 } from "react";
41753
41772
  import { Box as Box88, Text as Text83, useInput as useInput33 } from "ink";
41754
41773
  import { Fragment as Fragment13, jsx as jsx106, jsxs as jsxs84 } from "react/jsx-runtime";
41755
41774
  var CHROME_ROWS4 = 10;
@@ -41760,7 +41779,7 @@ var formatAgo = (modifiedAtMs, now) => {
41760
41779
  };
41761
41780
  var useProgressFile = (sprintId, dataRoot) => {
41762
41781
  const [state, setState] = useState53({ kind: "loading" });
41763
- useEffect49(() => {
41782
+ useEffect48(() => {
41764
41783
  let cancelled = false;
41765
41784
  if (sprintId === void 0) {
41766
41785
  setState({ kind: "missing" });
@@ -41775,7 +41794,7 @@ var useProgressFile = (sprintId, dataRoot) => {
41775
41794
  return;
41776
41795
  }
41777
41796
  const progressPath = join71(dir, "progress.md");
41778
- const [stat, content] = await Promise.all([fs36.stat(progressPath), fs36.readFile(progressPath, "utf8")]);
41797
+ const [stat, content] = await Promise.all([fs35.stat(progressPath), fs35.readFile(progressPath, "utf8")]);
41779
41798
  if (cancelled) return;
41780
41799
  const modifiedAtMs = stat.mtimeMs;
41781
41800
  if (content.trim().length === 0) {
@@ -41805,7 +41824,7 @@ var useProgressFile = (sprintId, dataRoot) => {
41805
41824
  var useProgressScroll = (state, bodyRows) => {
41806
41825
  const [offset, setOffset] = useState53(0);
41807
41826
  const lineCount = state.kind === "ok" ? state.lines.length : 0;
41808
- useEffect49(() => {
41827
+ useEffect48(() => {
41809
41828
  setOffset(0);
41810
41829
  }, [lineCount]);
41811
41830
  const maxOffset = Math.max(0, lineCount - bodyRows);
@@ -42002,7 +42021,7 @@ import { useState as useState55 } from "react";
42002
42021
  import { useApp as useApp2 } from "ink";
42003
42022
 
42004
42023
  // src/application/ui/tui/migration/migration-gate.tsx
42005
- import { useEffect as useEffect50, useRef as useRef19, useState as useState54 } from "react";
42024
+ import { useEffect as useEffect49, useRef as useRef19, useState as useState54 } from "react";
42006
42025
  import { Box as Box90, Text as Text84, useInput as useInput34 } from "ink";
42007
42026
 
42008
42027
  // src/application/ui/tui/migration/learnings-backfill-adapter.ts
@@ -42050,7 +42069,7 @@ var MigrationGate = (props) => {
42050
42069
  const { engine, dataRoot, stateRoot, appVersion, now, writeFile: writeFile3, onResolve, onQuit } = props;
42051
42070
  const [state, setState] = useState54({ kind: "scanning" });
42052
42071
  const scannedRef = useRef19(false);
42053
- useEffect50(() => {
42072
+ useEffect49(() => {
42054
42073
  if (scannedRef.current) return;
42055
42074
  scannedRef.current = true;
42056
42075
  let cancelled = false;
@@ -42296,7 +42315,7 @@ var MigrationRoute = ({ gate, app, onResolved }) => {
42296
42315
  };
42297
42316
 
42298
42317
  // src/integration/persistence/data-migration/version-marker.ts
42299
- import { promises as fs37 } from "fs";
42318
+ import { promises as fs36 } from "fs";
42300
42319
  import { join as join72 } from "path";
42301
42320
  import { z as z50 } from "zod";
42302
42321
  var CURRENT_DATA_VERSION = 2;
@@ -42309,7 +42328,7 @@ var versionMarkerPath = (dataRoot) => join72(String(dataRoot), DATA_VERSION_FILE
42309
42328
  var readDataVersion = async (dataRoot) => {
42310
42329
  let bytes;
42311
42330
  try {
42312
- bytes = await fs37.readFile(versionMarkerPath(dataRoot), "utf8");
42331
+ bytes = await fs36.readFile(versionMarkerPath(dataRoot), "utf8");
42313
42332
  } catch {
42314
42333
  return { dataVersion: 1, lastWrittenByAppVersion: "" };
42315
42334
  }
@@ -42331,8 +42350,8 @@ var writeDataVersion = async (dataRoot, marker) => {
42331
42350
  const body = `${JSON.stringify(marker, null, 2)}
42332
42351
  `;
42333
42352
  try {
42334
- await fs37.mkdir(String(dataRoot), { recursive: true });
42335
- await fs37.writeFile(path2, body, "utf8");
42353
+ await fs36.mkdir(String(dataRoot), { recursive: true });
42354
+ await fs36.writeFile(path2, body, "utf8");
42336
42355
  return Result.ok(void 0);
42337
42356
  } catch (cause) {
42338
42357
  return Result.error(new StorageError({ subCode: "io", message: `marker write failed: ${path2}`, path: path2, cause }));
@@ -42344,7 +42363,7 @@ var needsMigration = async (dataRoot) => {
42344
42363
  };
42345
42364
 
42346
42365
  // src/integration/persistence/data-migration/dry-run.ts
42347
- import { promises as fs38 } from "fs";
42366
+ import { promises as fs37 } from "fs";
42348
42367
  import { join as join73 } from "path";
42349
42368
  var PROJECTS_DIR = "projects";
42350
42369
  var SPRINTS_DIR = "sprints";
@@ -42363,7 +42382,7 @@ var dryRun = async (dataRoot) => {
42363
42382
  var checkBackupTargetWritable = async (dataRoot, acc) => {
42364
42383
  const parent = join73(String(dataRoot), "..");
42365
42384
  try {
42366
- await fs38.access(parent, fs38.constants.W_OK);
42385
+ await fs37.access(parent, fs37.constants.W_OK);
42367
42386
  } catch {
42368
42387
  acc.problems.push({
42369
42388
  name: parent,
@@ -42481,7 +42500,7 @@ var planRename = async (parent, fromName, toName, id, slug, kind, acc) => {
42481
42500
  };
42482
42501
  var pathPresent = async (path2) => {
42483
42502
  try {
42484
- await fs38.stat(path2);
42503
+ await fs37.stat(path2);
42485
42504
  return true;
42486
42505
  } catch {
42487
42506
  return false;
@@ -42489,7 +42508,7 @@ var pathPresent = async (path2) => {
42489
42508
  };
42490
42509
  var isDirectory = async (path2) => {
42491
42510
  try {
42492
- return (await fs38.stat(path2)).isDirectory();
42511
+ return (await fs37.stat(path2)).isDirectory();
42493
42512
  } catch {
42494
42513
  return false;
42495
42514
  }
@@ -42512,7 +42531,7 @@ var readProjectSlugForId = async (dataRoot, projectId) => {
42512
42531
  var readSlugField = async (filePath) => {
42513
42532
  let bytes;
42514
42533
  try {
42515
- bytes = await fs38.readFile(filePath, "utf8");
42534
+ bytes = await fs37.readFile(filePath, "utf8");
42516
42535
  } catch {
42517
42536
  return void 0;
42518
42537
  }
@@ -42529,11 +42548,11 @@ var readSlugField = async (filePath) => {
42529
42548
  };
42530
42549
 
42531
42550
  // src/integration/persistence/data-migration/apply.ts
42532
- import { promises as fs41 } from "fs";
42551
+ import { promises as fs40 } from "fs";
42533
42552
  import { join as join76 } from "path";
42534
42553
 
42535
42554
  // src/integration/persistence/data-migration/backup.ts
42536
- import { promises as fs39 } from "fs";
42555
+ import { promises as fs38 } from "fs";
42537
42556
  import { basename as basename7, dirname as dirname22, join as join74 } from "path";
42538
42557
  var BACKUP_DIR_PREFIX = "data.backup-";
42539
42558
  var backupDirName = (fromVersion, timestamp) => `${BACKUP_DIR_PREFIX}v${String(fromVersion)}-${sanitizeTimestamp(timestamp)}`;
@@ -42541,7 +42560,7 @@ var backupDataDir = async (dataRoot, fromVersion, timestamp) => {
42541
42560
  const src = String(dataRoot);
42542
42561
  const dest = join74(dirname22(src), backupDirName(fromVersion, timestamp));
42543
42562
  try {
42544
- await fs39.cp(src, dest, {
42563
+ await fs38.cp(src, dest, {
42545
42564
  recursive: true,
42546
42565
  // Follow symlinks so the backup captures their real CONTENT, not a dangling node. The default
42547
42566
  // (`dereference: false`) copies a symlink as a link, which would leave the backup incomplete for
@@ -42563,7 +42582,7 @@ var backupDataDir = async (dataRoot, fromVersion, timestamp) => {
42563
42582
  );
42564
42583
  }
42565
42584
  try {
42566
- const stat = await fs39.stat(dest);
42585
+ const stat = await fs38.stat(dest);
42567
42586
  if (!stat.isDirectory()) {
42568
42587
  return Result.error(
42569
42588
  new StorageError({ subCode: "io", message: `backup verify failed: ${dest} is not a directory`, path: dest })
@@ -42585,8 +42604,8 @@ var backupDataDir = async (dataRoot, fromVersion, timestamp) => {
42585
42604
  };
42586
42605
  var verifyEntryCount = async (src, dest) => {
42587
42606
  try {
42588
- const srcEntries = (await fs39.readdir(src)).filter((name) => !name.startsWith(BACKUP_DIR_PREFIX));
42589
- const destEntries = await fs39.readdir(dest);
42607
+ const srcEntries = (await fs38.readdir(src)).filter((name) => !name.startsWith(BACKUP_DIR_PREFIX));
42608
+ const destEntries = await fs38.readdir(dest);
42590
42609
  if (destEntries.length < srcEntries.length) {
42591
42610
  return Result.error(
42592
42611
  new StorageError({
@@ -42606,10 +42625,10 @@ var verifyEntryCount = async (src, dest) => {
42606
42625
  var sanitizeTimestamp = (timestamp) => timestamp.replace(/:/g, "-");
42607
42626
 
42608
42627
  // src/integration/persistence/data-migration/lock-guard.ts
42609
- import { promises as fs40 } from "fs";
42628
+ import { promises as fs39 } from "fs";
42610
42629
  import { join as join75 } from "path";
42611
42630
  var LOCKS_SUBDIR2 = "locks";
42612
- var HELD_WITHIN_MS = 3e4;
42631
+ var HELD_WITHIN_MS = DEFAULT_STALE_AFTER_MS;
42613
42632
  var anyLockHeld = async (stateRoot) => {
42614
42633
  const locksDir = join75(String(stateRoot), LOCKS_SUBDIR2);
42615
42634
  const entries = await listDir(locksDir);
@@ -42619,7 +42638,7 @@ var anyLockHeld = async (stateRoot) => {
42619
42638
  if (!name.endsWith(".lock")) continue;
42620
42639
  let mtimeMs;
42621
42640
  try {
42622
- ({ mtimeMs } = await fs40.lstat(join75(locksDir, name)));
42641
+ ({ mtimeMs } = await fs39.lstat(join75(locksDir, name)));
42623
42642
  } catch {
42624
42643
  continue;
42625
42644
  }
@@ -42712,7 +42731,7 @@ var mergeOne = async (merge, ctx) => {
42712
42731
  };
42713
42732
  var readTextOrEmpty = async (path2) => {
42714
42733
  try {
42715
- return await fs41.readFile(path2, "utf8");
42734
+ return await fs40.readFile(path2, "utf8");
42716
42735
  } catch {
42717
42736
  return "";
42718
42737
  }
@@ -42730,7 +42749,7 @@ var backfillLearningsMd = async (dataRoot, ctx) => {
42730
42749
  if (await pathPresent2(mdPath)) continue;
42731
42750
  let body;
42732
42751
  try {
42733
- body = await fs41.readFile(ledgerPath, "utf8");
42752
+ body = await fs40.readFile(ledgerPath, "utf8");
42734
42753
  } catch {
42735
42754
  continue;
42736
42755
  }
@@ -42743,7 +42762,7 @@ var backfillLearningsMd = async (dataRoot, ctx) => {
42743
42762
  };
42744
42763
  var ledgerExceedsCeiling = async (ledgerPath) => {
42745
42764
  try {
42746
- const { size } = await fs41.stat(ledgerPath);
42765
+ const { size } = await fs40.stat(ledgerPath);
42747
42766
  return size > MIRROR_BACKFILL_CEILING_BYTES;
42748
42767
  } catch {
42749
42768
  return false;
@@ -42751,7 +42770,7 @@ var ledgerExceedsCeiling = async (ledgerPath) => {
42751
42770
  };
42752
42771
  var pathPresent2 = async (path2) => {
42753
42772
  try {
42754
- await fs41.stat(path2);
42773
+ await fs40.stat(path2);
42755
42774
  return true;
42756
42775
  } catch {
42757
42776
  return false;
@@ -42797,7 +42816,7 @@ var resolveInitialState = ({
42797
42816
  };
42798
42817
 
42799
42818
  // src/integration/persistence/selection/last-selection-store.ts
42800
- import { promises as fs42 } from "fs";
42819
+ import { promises as fs41 } from "fs";
42801
42820
  import { join as join77 } from "path";
42802
42821
  var FILE_NAME = "last-selection.json";
42803
42822
  var createLastSelectionStore = (stateRoot) => {
@@ -42805,7 +42824,7 @@ var createLastSelectionStore = (stateRoot) => {
42805
42824
  return {
42806
42825
  async read() {
42807
42826
  try {
42808
- const raw = await fs42.readFile(path2, "utf8");
42827
+ const raw = await fs41.readFile(path2, "utf8");
42809
42828
  const parsed = JSON.parse(raw);
42810
42829
  if (typeof parsed !== "object" || parsed === null) return void 0;
42811
42830
  const rec = parsed;
@@ -42823,10 +42842,10 @@ var createLastSelectionStore = (stateRoot) => {
42823
42842
  async write(value) {
42824
42843
  try {
42825
42844
  if (value === void 0) {
42826
- await fs42.rm(path2, { force: true });
42845
+ await fs41.rm(path2, { force: true });
42827
42846
  return;
42828
42847
  }
42829
- await fs42.writeFile(path2, JSON.stringify(value, null, 2), "utf8");
42848
+ await fs41.writeFile(path2, JSON.stringify(value, null, 2), "utf8");
42830
42849
  } catch {
42831
42850
  }
42832
42851
  }
@@ -42925,13 +42944,13 @@ var startHeapWatchdog = (deps) => {
42925
42944
  };
42926
42945
 
42927
42946
  // src/integration/observability/heap-snapshot.ts
42928
- import * as fs43 from "fs";
42947
+ import * as fs42 from "fs";
42929
42948
  import * as path from "path";
42930
42949
  import * as v82 from "v8";
42931
42950
  var snapshotFilename = (timestamp) => `heap-${timestamp.replace(/[:.]/g, "-")}.heapsnapshot`;
42932
42951
  var writeHeapSnapshotToDir = (dir, clock) => {
42933
42952
  try {
42934
- fs43.mkdirSync(dir, { recursive: true });
42953
+ fs42.mkdirSync(dir, { recursive: true });
42935
42954
  const timestamp = clock?.() ?? (/* @__PURE__ */ new Date()).toISOString();
42936
42955
  const fullPath = path.join(dir, snapshotFilename(timestamp));
42937
42956
  v82.writeHeapSnapshot(fullPath);
@@ -44452,11 +44471,11 @@ var formatTaskLine = (t) => {
44452
44471
  };
44453
44472
 
44454
44473
  // src/application/ui/cli/commands/runs.ts
44455
- import { promises as fs45 } from "fs";
44474
+ import { promises as fs44 } from "fs";
44456
44475
  import { createInterface as createInterface2 } from "readline";
44457
44476
 
44458
44477
  // src/integration/ai/runs/_engine/run-enumeration.ts
44459
- import { promises as fs44 } from "fs";
44478
+ import { promises as fs43 } from "fs";
44460
44479
  import { join as join80 } from "path";
44461
44480
  var parseRunTimestamp = (runDirName) => {
44462
44481
  const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z-/.exec(runDirName);
@@ -44557,7 +44576,7 @@ var listRuns = async (runsRoot) => {
44557
44576
  const root = String(runsRoot);
44558
44577
  let flowDirs;
44559
44578
  try {
44560
- flowDirs = await fs44.readdir(root, { withFileTypes: true });
44579
+ flowDirs = await fs43.readdir(root, { withFileTypes: true });
44561
44580
  } catch (cause) {
44562
44581
  if (isErrnoException2(cause) && cause.code === "ENOENT") return Result.ok([]);
44563
44582
  return Result.error(
@@ -44574,7 +44593,7 @@ var listRuns = async (runsRoot) => {
44574
44593
  const flowPath = join80(root, flowDir.name);
44575
44594
  let runDirs;
44576
44595
  try {
44577
- runDirs = await fs44.readdir(flowPath, { withFileTypes: true });
44596
+ runDirs = await fs43.readdir(flowPath, { withFileTypes: true });
44578
44597
  } catch (cause) {
44579
44598
  if (isErrnoException2(cause) && cause.code === "ENOENT") continue;
44580
44599
  return Result.error(
@@ -44619,7 +44638,7 @@ var computeDirSize = async (dir) => {
44619
44638
  let total = 0;
44620
44639
  let entries;
44621
44640
  try {
44622
- entries = await fs44.readdir(dir, { withFileTypes: true });
44641
+ entries = await fs43.readdir(dir, { withFileTypes: true });
44623
44642
  } catch {
44624
44643
  return 0;
44625
44644
  }
@@ -44630,7 +44649,7 @@ var computeDirSize = async (dir) => {
44630
44649
  continue;
44631
44650
  }
44632
44651
  try {
44633
- const stat = await fs44.lstat(entryPath);
44652
+ const stat = await fs43.lstat(entryPath);
44634
44653
  if (stat.isFile()) total += stat.size;
44635
44654
  } catch {
44636
44655
  }
@@ -44951,7 +44970,7 @@ var performPrune = async (candidates) => {
44951
44970
  let freedCount = 0;
44952
44971
  for (const candidate of candidates) {
44953
44972
  try {
44954
- await fs45.rm(String(candidate.path), { recursive: true, force: false });
44973
+ await fs44.rm(String(candidate.path), { recursive: true, force: false });
44955
44974
  freedBytes += candidate.sizeBytes;
44956
44975
  freedCount += 1;
44957
44976
  } catch (cause) {