lazycodex-ai 5.0.0-beta.45 → 5.0.0-beta.47

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.
Files changed (99) hide show
  1. package/dist/cli/index.js +43 -27
  2. package/dist/cli-node/index.js +43 -27
  3. package/package.json +1 -1
  4. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  5. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  6. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  7. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  8. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  9. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  10. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  11. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  12. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  13. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  14. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  15. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  16. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  17. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  18. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  19. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  20. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  21. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  22. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  23. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
  25. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  26. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +15 -7
  27. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -1
  28. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +1 -1
  29. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +294 -96
  30. package/packages/omo-codex/plugin/components/ulw-loop/dist/constants.d.ts +1 -0
  31. package/packages/omo-codex/plugin/components/ulw-loop/dist/constants.js +1 -0
  32. package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.d.ts +1 -0
  33. package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.js +12 -3
  34. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +1 -0
  35. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +21 -6
  36. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +1 -0
  37. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +10 -0
  38. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.d.ts +4 -1
  39. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +25 -57
  40. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.d.ts +9 -0
  41. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.js +227 -0
  42. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +17 -3
  43. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  44. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +2 -1
  46. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +10 -7
  47. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +18 -7
  48. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +1 -1
  49. package/packages/omo-codex/plugin/components/ulw-loop/src/constants.ts +1 -0
  50. package/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts +24 -3
  51. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +25 -5
  52. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +11 -0
  53. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +34 -64
  54. package/packages/omo-codex/plugin/components/ulw-loop/src/state-lock.ts +239 -0
  55. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +16 -3
  56. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-template.test.ts +14 -4
  57. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-checkpoint-continuation.test.ts +4 -2
  58. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-checkpoint.test.ts +4 -2
  59. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-commands.test.ts +2 -0
  60. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-complete-goals.test.ts +3 -1
  61. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-create-goals.test.ts +10 -8
  62. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-entrypoint.test.ts +12 -2
  63. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-json-errors.test.ts +2 -0
  64. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-scope-required.test.ts +144 -0
  65. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-status-next-actions.test.ts +7 -5
  66. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-steering-batch.test.ts +8 -3
  67. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-validation-batch.test.ts +6 -0
  68. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/cli-session.ts +4 -0
  69. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/quality-gate-builder.ts +2 -1
  70. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io-cross-process.test.ts +106 -0
  71. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +36 -2
  72. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +37 -0
  73. package/packages/omo-codex/plugin/components/ulw-loop/test/state-lock.test.ts +218 -0
  74. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  75. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  76. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  77. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  78. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  79. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  80. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  81. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  82. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  83. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  84. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  85. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  86. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  87. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  88. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  89. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  90. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
  91. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  92. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  93. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  94. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  95. package/packages/omo-codex/plugin/package-lock.json +12 -12
  96. package/packages/omo-codex/plugin/package.json +1 -1
  97. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +2 -1
  98. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +10 -7
  99. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // components/ulw-loop/src/checkpoint.ts
4
- import { existsSync as existsSync4, statSync } from "node:fs";
4
+ import { existsSync as existsSync4, statSync as statSync2 } from "node:fs";
5
5
  import { readFile as readFile5 } from "node:fs/promises";
6
6
  import { resolve as resolve3 } from "node:path";
7
7
 
@@ -123,6 +123,7 @@ var ULW_LOOP_DIR = ".omo/ulw-loop";
123
123
  var ULW_LOOP_BRIEF = "brief.md";
124
124
  var ULW_LOOP_GOALS = "goals.json";
125
125
  var ULW_LOOP_LEDGER = "ledger.jsonl";
126
+ var ULW_LOOP_STATE_LOCK = ".state.lock";
126
127
  var ULW_LOOP_STEERING_MUTATION_KINDS = [
127
128
  "add_subgoal",
128
129
  "split_subgoal",
@@ -195,6 +196,9 @@ function ulwLoopGoalsPath(repoRoot, scope) {
195
196
  function ulwLoopLedgerPath(repoRoot, scope) {
196
197
  return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_LEDGER);
197
198
  }
199
+ function ulwLoopStateLockPath(repoRoot, scope) {
200
+ return join(ulwLoopDir(repoRoot, scope), ULW_LOOP_STATE_LOCK);
201
+ }
198
202
  function repoRelative(absolutePath, repoRoot) {
199
203
  const slashPrefix = `${repoRoot}/`;
200
204
  const backslashPrefix = `${repoRoot}\\`;
@@ -205,7 +209,10 @@ function repoRelative(absolutePath, repoRoot) {
205
209
  return absolutePath.split("\\").join("/");
206
210
  }
207
211
  function ulwLoopAttemptEvidenceDir(goalId, attempt, scope) {
208
- const sessionId = normalizeUlwLoopSessionId(scope?.sessionId) ?? resolveUlwLoopSessionIdFromEnv() ?? "session";
212
+ const sessionId = normalizeUlwLoopSessionId(scope?.sessionId);
213
+ if (sessionId === null) {
214
+ throw new UlwLoopError(`Evidence for ${goalId} attempt ${attempt} needs a session scope; pass --session-id <id> so the attempt directory lives under .omo/evidence/ulw/<id>/.`, "ULW_LOOP_SESSION_SCOPE_REQUIRED", { details: { goalId, attempt } });
215
+ }
209
216
  return `.omo/evidence/ulw/${sessionId}/${goalId}/a${attempt}`;
210
217
  }
211
218
  var PLATFORM_PATH_API = { relative, isAbsolute, sep };
@@ -418,6 +425,7 @@ ${gateError.message}`, "ULW_LOOP_QUALITY_GATE_INVALID", {
418
425
  }
419
426
 
420
427
  // components/ulw-loop/src/plan-io.ts
428
+ import { AsyncLocalStorage } from "node:async_hooks";
421
429
  import { createReadStream, readdirSync } from "node:fs";
422
430
  import { appendFile, mkdir, readFile as readFile3, rename, writeFile } from "node:fs/promises";
423
431
  import { createInterface } from "node:readline";
@@ -433,6 +441,17 @@ function planMissingRecovery(existingSessionIds) {
433
441
  return { message: lines.join(`
434
442
  `), details: { existingSessionIds } };
435
443
  }
444
+ function sessionScopeRequiredMessage(flag, existingSessionIds) {
445
+ const lines = [
446
+ "No ulw-loop session scope: neither the session env (OMO_ULW_LOOP_SESSION_ID / CODEX_SESSION_ID / CODEX_THREAD_ID / PI_SESSION_ID) nor the flag names this run, and the shared .omo/ulw-loop root is never used implicitly because every session in this directory would read and overwrite it.",
447
+ `Recovery: pass the scope explicitly: \`${flag} <id>\` (subprocess, eval, and hook contexts do not inherit the session env).`
448
+ ];
449
+ if (existingSessionIds.length > 0) {
450
+ lines.push(`Existing ulw-loop session ids under .omo/ulw-loop/: ${existingSessionIds.join(", ")}.`);
451
+ }
452
+ return lines.join(`
453
+ `);
454
+ }
436
455
  function sessionIdRequiredMessage(flag) {
437
456
  return [
438
457
  `${flag} requires a non-empty value.`,
@@ -442,11 +461,205 @@ function sessionIdRequiredMessage(flag) {
442
461
  `);
443
462
  }
444
463
 
464
+ // components/ulw-loop/src/state-lock.ts
465
+ import { randomUUID } from "node:crypto";
466
+ import { closeSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeSync } from "node:fs";
467
+ import { dirname } from "node:path";
468
+ import { setTimeout as sleep } from "node:timers/promises";
469
+ var ULW_LOOP_LOCK_TIMEOUT_CODE = "ULW_LOOP_LOCK_TIMEOUT";
470
+ var DEFAULT_TIMEOUT_MS = 1e4;
471
+ var DEFAULT_STALE_MS = 60000;
472
+ var MIN_DELAY_MS = 5;
473
+ var MAX_DELAY_MS = 100;
474
+ var SLEEP_CELL = new Int32Array(new SharedArrayBuffer(4));
475
+ async function withStateLock(lockPath, fn, options = {}) {
476
+ const token = await acquireAsync(lockPath, options);
477
+ try {
478
+ return await fn();
479
+ } finally {
480
+ release(lockPath, token);
481
+ }
482
+ }
483
+ function withStateLockSync(lockPath, fn, options = {}) {
484
+ const token = acquireSync(lockPath, options);
485
+ try {
486
+ return fn();
487
+ } finally {
488
+ release(lockPath, token);
489
+ }
490
+ }
491
+ function isStateLockTimeout(error) {
492
+ return error instanceof UlwLoopError && error.code === ULW_LOOP_LOCK_TIMEOUT_CODE;
493
+ }
494
+ async function acquireAsync(lockPath, options) {
495
+ const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
496
+ const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
497
+ mkdirSync(dirname(lockPath), { recursive: true });
498
+ for (let attempt = 0;; ) {
499
+ const outcome = attemptOnce(lockPath, staleMs);
500
+ if (outcome.kind === "acquired")
501
+ return outcome.token;
502
+ if (outcome.kind === "retry")
503
+ continue;
504
+ if (Date.now() >= deadline)
505
+ throw lockTimeout(lockPath, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
506
+ await sleep(backoffMs(attempt));
507
+ attempt += 1;
508
+ }
509
+ }
510
+ function acquireSync(lockPath, options) {
511
+ const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
512
+ const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
513
+ mkdirSync(dirname(lockPath), { recursive: true });
514
+ for (let attempt = 0;; ) {
515
+ const outcome = attemptOnce(lockPath, staleMs);
516
+ if (outcome.kind === "acquired")
517
+ return outcome.token;
518
+ if (outcome.kind === "retry")
519
+ continue;
520
+ if (Date.now() >= deadline)
521
+ throw lockTimeout(lockPath, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
522
+ Atomics.wait(SLEEP_CELL, 0, 0, backoffMs(attempt));
523
+ attempt += 1;
524
+ }
525
+ }
526
+ function attemptOnce(lockPath, staleMs) {
527
+ try {
528
+ const token = tryCreate(lockPath);
529
+ if (token !== null)
530
+ return { kind: "acquired", token };
531
+ const snapshot = readSnapshot(lockPath);
532
+ if (snapshot === null)
533
+ return { kind: "retry" };
534
+ if (isStale(snapshot, staleMs) && reclaim(lockPath, snapshot.raw))
535
+ return { kind: "retry" };
536
+ return { kind: "wait" };
537
+ } catch (error) {
538
+ if (hasCode(error, "EINTR"))
539
+ return { kind: "wait" };
540
+ throw error;
541
+ }
542
+ }
543
+ function tryCreate(lockPath) {
544
+ let fd;
545
+ try {
546
+ fd = openSync(lockPath, "wx");
547
+ } catch (error) {
548
+ if (hasCode(error, "EEXIST"))
549
+ return null;
550
+ throw error;
551
+ }
552
+ const record = { pid: process.pid, createdAt: new Date().toISOString(), token: randomUUID() };
553
+ try {
554
+ writeSync(fd, JSON.stringify(record));
555
+ } catch (error) {
556
+ closeSync(fd);
557
+ try {
558
+ unlinkSync(lockPath);
559
+ } catch {}
560
+ throw error;
561
+ }
562
+ closeSync(fd);
563
+ return record.token;
564
+ }
565
+ function readSnapshot(lockPath) {
566
+ try {
567
+ const raw = readFileSync(lockPath, "utf8");
568
+ const ageMs = Date.now() - statSync(lockPath).mtimeMs;
569
+ return { raw, record: parseRecord(raw), ageMs };
570
+ } catch (error) {
571
+ if (hasCode(error, "ENOENT"))
572
+ return null;
573
+ throw error;
574
+ }
575
+ }
576
+ function parseRecord(raw) {
577
+ try {
578
+ const parsed = JSON.parse(raw);
579
+ if (typeof parsed !== "object" || parsed === null)
580
+ return null;
581
+ const record = parsed;
582
+ const pid = record["pid"];
583
+ const createdAt = record["createdAt"];
584
+ const token = record["token"];
585
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0 || typeof createdAt !== "string")
586
+ return null;
587
+ if (typeof token !== "string" || token.length === 0)
588
+ return null;
589
+ return { pid, createdAt, token };
590
+ } catch (error) {
591
+ if (error instanceof SyntaxError)
592
+ return null;
593
+ throw error;
594
+ }
595
+ }
596
+ function isStale(snapshot, staleMs) {
597
+ if (snapshot.record === null)
598
+ return snapshot.ageMs > staleMs;
599
+ return !isProcessAlive(snapshot.record.pid);
600
+ }
601
+ function isProcessAlive(pid) {
602
+ try {
603
+ process.kill(pid, 0);
604
+ return true;
605
+ } catch (error) {
606
+ if (hasCode(error, "ESRCH"))
607
+ return false;
608
+ if (hasCode(error, "EPERM"))
609
+ return true;
610
+ throw error;
611
+ }
612
+ }
613
+ function reclaim(lockPath, expectedRaw) {
614
+ const current = readSnapshot(lockPath);
615
+ if (current === null)
616
+ return true;
617
+ if (current.raw !== expectedRaw)
618
+ return false;
619
+ try {
620
+ unlinkSync(lockPath);
621
+ } catch (error) {
622
+ if (!hasCode(error, "ENOENT"))
623
+ throw error;
624
+ }
625
+ return true;
626
+ }
627
+ function release(lockPath, token) {
628
+ const current = readSnapshot(lockPath);
629
+ if (current === null || current.record?.token !== token)
630
+ return;
631
+ try {
632
+ unlinkSync(lockPath);
633
+ } catch (error) {
634
+ if (!hasCode(error, "ENOENT"))
635
+ throw error;
636
+ }
637
+ }
638
+ function backoffMs(attempt) {
639
+ const exponential = Math.min(MAX_DELAY_MS, MIN_DELAY_MS * 2 ** attempt);
640
+ return exponential + Math.random() * MIN_DELAY_MS;
641
+ }
642
+ function lockTimeout(lockPath, timeoutMs) {
643
+ const holder = readSnapshot(lockPath)?.record;
644
+ const owner = holder === undefined || holder === null ? "another process" : `pid ${holder.pid}`;
645
+ return new UlwLoopError(`ulw-loop state lock ${lockPath} is held by ${owner} for more than ${timeoutMs}ms; retry once that process finishes, or delete the lock file if that process is gone.`, ULW_LOOP_LOCK_TIMEOUT_CODE, {
646
+ details: {
647
+ lockPath,
648
+ timeoutMs,
649
+ ...holder === undefined || holder === null ? {} : { holderPid: holder.pid }
650
+ }
651
+ });
652
+ }
653
+ function hasCode(error, code) {
654
+ return error instanceof Error && "code" in error && error.code === code;
655
+ }
656
+
445
657
  // components/ulw-loop/src/plan-io.ts
446
658
  var LEGACY_OBJECTIVE_PREFIX = `Complete all ulw-loop stories in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}: `;
447
659
  var LEGACY_OBJECTIVE = `Complete all ulw-loop stories listed in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}. Use ${ULW_LOOP_DIR}/${ULW_LOOP_LEDGER} as the durable audit trail.`;
448
660
  var locks = new Map;
449
- function hasCode(error, code) {
661
+ var heldLocks = new AsyncLocalStorage;
662
+ function hasCode2(error, code) {
450
663
  return error instanceof Error && "code" in error && error.code === code;
451
664
  }
452
665
  function isLegacyEnumeratedAggregateObjective(objective) {
@@ -461,8 +674,10 @@ async function withUlwLoopMutationLock(repoRoot, scopeOrFn, maybeFn) {
461
674
  if (fn === undefined)
462
675
  throw new UlwLoopError("Missing ulw-loop mutation body.", "ULW_LOOP_LOCK_BODY_MISSING");
463
676
  const lockKey = `${repoRoot}\x00${ulwLoopRelativeDir(scope)}`;
677
+ const lockPath = ulwLoopStateLockPath(repoRoot, scope);
678
+ const locked = () => withStateLock(lockPath, () => heldLocks.run(new Set([...heldLocks.getStore() ?? [], lockKey]), fn));
464
679
  const prior = locks.get(lockKey) ?? Promise.resolve(undefined);
465
- const run = prior.then(fn, fn);
680
+ const run = prior.then(locked, locked);
466
681
  const gate = run.then(() => {
467
682
  return;
468
683
  }, () => {
@@ -481,9 +696,9 @@ async function readUlwLoopPlan(repoRoot, scope) {
481
696
  try {
482
697
  raw = await readFile3(path, "utf8");
483
698
  } catch (error) {
484
- if (!hasCode(error, "ENOENT"))
699
+ if (!hasCode2(error, "ENOENT"))
485
700
  throw error;
486
- const recovery = planMissingRecovery(readSessionDirs(repoRoot));
701
+ const recovery = planMissingRecovery(listUlwLoopSessionIds(repoRoot));
487
702
  throw new UlwLoopError(`No ulw-loop plan found at ${repoRelative(path, repoRoot)}.
488
703
  ${recovery.message}`, "ULW_LOOP_PLAN_MISSING", { cause: error, ...recovery.details === undefined ? {} : { details: recovery.details } });
489
704
  }
@@ -493,6 +708,9 @@ ${recovery.message}`, "ULW_LOOP_PLAN_MISSING", { cause: error, ...recovery.detai
493
708
  }
494
709
  const previousObjective = parsed.codexObjective;
495
710
  if ((parsed.codexGoalMode ?? "per_story") === "aggregate" && isLegacyEnumeratedAggregateObjective(previousObjective)) {
711
+ if (!(heldLocks.getStore()?.has(`${repoRoot}\x00${ulwLoopRelativeDir(scope)}`) ?? false)) {
712
+ throw new UlwLoopError(`The ulw-loop plan at ${repoRelative(path, repoRoot)} carries a legacy enumerated aggregate objective that must be migrated before reads continue. Run any state-mutating ulw-loop command once (e.g. \`record-evidence\`, \`steer\`, \`checkpoint\`) to migrate it under the state lock, then retry.`, "ULW_LOOP_MIGRATION_REQUIRED");
713
+ }
496
714
  const now = iso();
497
715
  parsed.codexObjective = aggregateCodexObjectiveForScope(scope);
498
716
  parsed.codexObjectiveAliases = [...new Set([...parsed.codexObjectiveAliases ?? [], previousObjective])];
@@ -508,7 +726,7 @@ ${recovery.message}`, "ULW_LOOP_PLAN_MISSING", { cause: error, ...recovery.detai
508
726
  }
509
727
  return parsed;
510
728
  }
511
- function readSessionDirs(repoRoot) {
729
+ function listUlwLoopSessionIds(repoRoot) {
512
730
  try {
513
731
  return readdirSync(ulwLoopDir(repoRoot), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
514
732
  } catch {
@@ -543,7 +761,7 @@ async function* ledgerLines(repoRoot, scope) {
543
761
  yield line;
544
762
  }
545
763
  } catch (error) {
546
- if (!hasCode(error, "ENOENT"))
764
+ if (!hasCode2(error, "ENOENT"))
547
765
  throw error;
548
766
  } finally {
549
767
  lines.close();
@@ -895,8 +1113,8 @@ function adversarialVerdict(row, field) {
895
1113
  }
896
1114
 
897
1115
  // components/ulw-loop/src/surface.ts
898
- import { existsSync as existsSync3, readFileSync } from "node:fs";
899
- import { dirname, join as join2 } from "node:path";
1116
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
1117
+ import { dirname as dirname2, join as join2 } from "node:path";
900
1118
  import { fileURLToPath } from "node:url";
901
1119
  var REVIEWER_ROLES_BY_SURFACE = {
902
1120
  lazycodex: {
@@ -935,7 +1153,7 @@ function resolveToolkitSurface(options) {
935
1153
  const fromEnv = parseSurface(env[SURFACE_ENV_KEY]);
936
1154
  if (fromEnv !== null)
937
1155
  return fromEnv;
938
- const entryDir = options?.entryDir ?? dirname(fileURLToPath(import.meta.url));
1156
+ const entryDir = options?.entryDir ?? dirname2(fileURLToPath(import.meta.url));
939
1157
  const markerPath = join2(entryDir, SURFACE_MARKER_FILENAME);
940
1158
  return readMarkerSurface(markerPath) ?? "lazycodex";
941
1159
  }
@@ -943,7 +1161,7 @@ function readMarkerSurface(markerPath) {
943
1161
  try {
944
1162
  if (!existsSync3(markerPath))
945
1163
  return null;
946
- const parsed = JSON.parse(readFileSync(markerPath, "utf8"));
1164
+ const parsed = JSON.parse(readFileSync2(markerPath, "utf8"));
947
1165
  return parseSurface(parsed["surface"]);
948
1166
  } catch (error) {
949
1167
  if (error instanceof Error)
@@ -1356,7 +1574,7 @@ function fail(message, code = "ULW_LOOP_VALIDATION_BATCH_INVALID") {
1356
1574
  }
1357
1575
 
1358
1576
  // components/ulw-loop/src/checkpoint.ts
1359
- var QUALITY_GATE_FS = { existsSync: existsSync4, statSync };
1577
+ var QUALITY_GATE_FS = { existsSync: existsSync4, statSync: statSync2 };
1360
1578
  function ulwLoopFail2(message, code) {
1361
1579
  throw new UlwLoopError(message, code);
1362
1580
  }
@@ -1659,7 +1877,7 @@ var ULW_LOOP_HELP = `Usage:
1659
1877
  omo-agent-toolkit ulw-loop add-goal --title "..." --objective "..." [--json]
1660
1878
  omo-agent-toolkit ulw-loop record-review-blockers --goal-id <id> --title "..." --objective "..." --evidence "..." --codex-goal-json <...> [--json]
1661
1879
 
1662
- All subcommands accept [--session-id <id>] to isolate state under .omo/ulw-loop/<id>/; without it, Codex session env is used when present.
1880
+ Every state subcommand needs a session scope: [--session-id <id>] or the session env (OMO_ULW_LOOP_SESSION_ID / CODEX_SESSION_ID / CODEX_THREAD_ID / PI_SESSION_ID); state lives under .omo/ulw-loop/<id>/ and the unscoped root is never used implicitly.
1663
1881
  Every subcommand accepts --help | -h to print its own usage line.`;
1664
1882
  function subcommandHelp(subcommand) {
1665
1883
  const lines = ULW_LOOP_HELP.split(`
@@ -3171,7 +3389,6 @@ async function ulwLoopCommand(argv) {
3171
3389
  const repoRoot = process.cwd();
3172
3390
  const json = hasFlag(rest, "--json");
3173
3391
  try {
3174
- const scope = commandScope(rest);
3175
3392
  if (!isUlwLoopSubcommand(command)) {
3176
3393
  if (json) {
3177
3394
  printJsonError(new UlwLoopError(`Unknown ulw-loop subcommand: ${command}.`, "ULW_LOOP_SUBCOMMAND_UNKNOWN", {
@@ -3188,11 +3405,13 @@ async function ulwLoopCommand(argv) {
3188
3405
  `);
3189
3406
  return 0;
3190
3407
  }
3191
- switch (command) {
3192
- case "help":
3193
- process.stdout.write(`${ULW_LOOP_HELP}
3408
+ if (command === "help") {
3409
+ process.stdout.write(`${ULW_LOOP_HELP}
3194
3410
  `);
3195
- return 0;
3411
+ return 0;
3412
+ }
3413
+ const scope = commandScope(repoRoot, rest);
3414
+ switch (command) {
3196
3415
  case "create-goals":
3197
3416
  return await createGoals(repoRoot, rest, json, scope);
3198
3417
  case "status":
@@ -3238,7 +3457,7 @@ var SESSION_ID_FLAG = "--session-id";
3238
3457
  function sessionIdFlagPresent(argv) {
3239
3458
  return hasFlag(argv, SESSION_ID_FLAG) || argv.some((arg) => arg.startsWith(`${SESSION_ID_FLAG}=`));
3240
3459
  }
3241
- function commandScope(argv) {
3460
+ function commandScope(repoRoot, argv) {
3242
3461
  if (sessionIdFlagPresent(argv)) {
3243
3462
  const sessionId2 = readValue(argv, SESSION_ID_FLAG)?.trim();
3244
3463
  if (!sessionId2) {
@@ -3249,14 +3468,17 @@ function commandScope(argv) {
3249
3468
  return { sessionId: sessionId2 };
3250
3469
  }
3251
3470
  const sessionId = resolveUlwLoopSessionIdFromEnv();
3252
- return sessionId === null ? undefined : { sessionId };
3471
+ if (sessionId !== null)
3472
+ return { sessionId };
3473
+ const existingSessionIds = listUlwLoopSessionIds(repoRoot);
3474
+ throw new UlwLoopError(sessionScopeRequiredMessage(SESSION_ID_FLAG, existingSessionIds), "ULW_LOOP_SESSION_SCOPE_REQUIRED", { details: { flag: SESSION_ID_FLAG, existingSessionIds } });
3253
3475
  }
3254
3476
 
3255
3477
  // components/ulw-loop/src/ultrawork-directive.ts
3256
- import { readFileSync as readFileSync3 } from "node:fs";
3478
+ import { readFileSync as readFileSync4 } from "node:fs";
3257
3479
 
3258
3480
  // components/ulw-loop/src/ultrawork-skill-pointer.ts
3259
- import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
3481
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
3260
3482
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3261
3483
  var ULTRAWORK_SKILL_POINTER_TEMPLATE = `<ultrawork-mode>
3262
3484
  ULTRAWORK MODE IS ACTIVE FOR THIS TASK.
@@ -3288,7 +3510,7 @@ Do not start the requested work until all three steps are complete.
3288
3510
  `;
3289
3511
  var ULTRAWORK_SKILL_PATH_PLACEHOLDER = "{{ULTRAWORK_SKILL_PATH}}";
3290
3512
  var ULTRAWORK_SKILL_FILE_URL = new URL("../../../skills/ultrawork/SKILL.md", import.meta.url);
3291
- var ULTRAWORK_DIRECTIVE = readFileSync2(new URL("../directive.md", import.meta.url), "utf8");
3513
+ var ULTRAWORK_DIRECTIVE = readFileSync3(new URL("../directive.md", import.meta.url), "utf8");
3292
3514
  function resolveUltraworkSkillFilePath() {
3293
3515
  return fileURLToPath2(ULTRAWORK_SKILL_FILE_URL);
3294
3516
  }
@@ -3351,7 +3573,7 @@ function hasUltraworkDirectiveAlreadyInTranscript(transcriptPath) {
3351
3573
  return false;
3352
3574
  }
3353
3575
  function readTranscriptTail(transcriptPath) {
3354
- const rawTranscript = readFileSync3(transcriptPath);
3576
+ const rawTranscript = readFileSync4(transcriptPath);
3355
3577
  return rawTranscript.subarray(Math.max(0, rawTranscript.byteLength - TRANSCRIPT_SEARCH_BYTES)).toString("utf8");
3356
3578
  }
3357
3579
  function isUltraworkPrompt(prompt) {
@@ -3545,18 +3767,8 @@ function readAll(stdin) {
3545
3767
 
3546
3768
  // components/ulw-loop/src/spawn-guard.ts
3547
3769
  import { randomBytes } from "node:crypto";
3548
- import {
3549
- existsSync as existsSync7,
3550
- mkdirSync,
3551
- openSync,
3552
- readdirSync as readdirSync2,
3553
- readFileSync as readFileSync4,
3554
- renameSync,
3555
- statSync as statSync2,
3556
- unlinkSync,
3557
- writeFileSync
3558
- } from "node:fs";
3559
- import { dirname as dirname2, join as join3 } from "node:path";
3770
+ import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync5, renameSync, statSync as statSync3, writeFileSync } from "node:fs";
3771
+ import { dirname as dirname3, join as join3 } from "node:path";
3560
3772
  var SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
3561
3773
  var DEFAULT_FANOUT_LIMIT = 60;
3562
3774
  var DEFAULT_REVIEW_SPAWN_LIMIT = 3;
@@ -3567,13 +3779,24 @@ var REVIEW_AGENT_TYPES = [
3567
3779
  ...Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.manualQa)
3568
3780
  ];
3569
3781
  var REVIEW_AGENT_TYPE_SET = new Set(REVIEW_AGENT_TYPES);
3570
- function applySpawnGuards(payload) {
3782
+ function applySpawnGuards(payload, options = {}) {
3571
3783
  if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
3572
3784
  return "";
3573
- const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
3785
+ const scope = { sessionId: payload.session_id };
3786
+ const stateDir = ulwLoopDir(payload.cwd, scope);
3574
3787
  const plan = readPlan(join3(stateDir, "goals.json"));
3575
3788
  if (plan === null)
3576
3789
  return "";
3790
+ const lockOptions = options.lockTimeoutMs === undefined ? {} : { timeoutMs: options.lockTimeoutMs };
3791
+ try {
3792
+ return withStateLockSync(ulwLoopStateLockPath(payload.cwd, scope), () => evaluateGuards(payload, plan, stateDir), lockOptions);
3793
+ } catch (error) {
3794
+ if (isStateLockTimeout(error))
3795
+ return deny(`ulw-loop spawn guard could not take the session state lock: ${error.message}`);
3796
+ throw error;
3797
+ }
3798
+ }
3799
+ function evaluateGuards(payload, plan, stateDir) {
3577
3800
  const fanOutPeek = peekFanOutBudget(stateDir);
3578
3801
  if (fanOutPeek !== null)
3579
3802
  return deny(fanOutPeek);
@@ -3629,18 +3852,15 @@ function consumeReviewSpawnBudget(payload, plan, stateDir) {
3629
3852
  if (goal3 === undefined)
3630
3853
  return null;
3631
3854
  const counterPath = join3(stateDir, "review-spawn-counts.json");
3632
- const lockPath = `${counterPath}.lock`;
3633
3855
  const limit = reviewSpawnLimit();
3634
- return withExclusiveLock(lockPath, () => {
3635
- const counts = readCounts(counterPath);
3636
- const key = `${agentType}:${goal3.id}:a${goal3.attempt}`;
3637
- const count = (counts[key] ?? 0) + 1;
3638
- if (count > limit)
3639
- return `ulw-loop reviewer no-progress cap reached (${agentType} ${count}/${limit}) for ${goal3.id} attempt ${goal3.attempt}. Consolidate existing review findings, or checkpoint and start a new attempt after concrete progress.`;
3640
- counts[key] = count;
3641
- atomicWriteJson(counterPath, counts);
3642
- return null;
3643
- });
3856
+ const counts = readCounts(counterPath);
3857
+ const key = `${agentType}:${goal3.id}:a${goal3.attempt}`;
3858
+ const count = (counts[key] ?? 0) + 1;
3859
+ if (count > limit)
3860
+ return `ulw-loop reviewer no-progress cap reached (${agentType} ${count}/${limit}) for ${goal3.id} attempt ${goal3.attempt}. Consolidate existing review findings, or checkpoint and start a new attempt after concrete progress.`;
3861
+ counts[key] = count;
3862
+ atomicWriteJson(counterPath, counts);
3863
+ return null;
3644
3864
  }
3645
3865
  function missingGateArtifact(payload, plan) {
3646
3866
  if (!isGateReviewerSpawn(payload.tool_input))
@@ -3718,40 +3938,8 @@ function activeSurfaceReviewerAlias(reviewer) {
3718
3938
  }
3719
3939
  return reviewer;
3720
3940
  }
3721
- function withExclusiveLock(lockPath, fn) {
3722
- mkdirSync(dirname2(lockPath), { recursive: true });
3723
- const maxAttempts = 10;
3724
- const baseDelayMs = 10;
3725
- for (let attempt = 0;attempt < maxAttempts; attempt++) {
3726
- let fd = null;
3727
- try {
3728
- fd = openSync(lockPath, "wx");
3729
- writeFileSync(fd, process.pid.toString());
3730
- try {
3731
- return fn();
3732
- } finally {
3733
- try {
3734
- unlinkSync(lockPath);
3735
- } catch {}
3736
- }
3737
- } catch (error) {
3738
- if (fd !== null) {
3739
- try {
3740
- unlinkSync(lockPath);
3741
- } catch {}
3742
- }
3743
- if (error.code !== "EEXIST") {
3744
- return fn();
3745
- }
3746
- const delayMs = baseDelayMs * 2 ** attempt + Math.random() * baseDelayMs;
3747
- const deadline = Date.now() + delayMs;
3748
- while (Date.now() < deadline) {}
3749
- }
3750
- }
3751
- return fn();
3752
- }
3753
3941
  function atomicWriteJson(targetPath, data) {
3754
- const tmp = join3(dirname2(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
3942
+ const tmp = join3(dirname3(targetPath), `.tmp-${randomBytes(6).toString("hex")}`);
3755
3943
  writeFileSync(tmp, JSON.stringify(data));
3756
3944
  renameSync(tmp, targetPath);
3757
3945
  }
@@ -3782,7 +3970,7 @@ function reviewSpawnLimit() {
3782
3970
  }
3783
3971
  function isNonEmptyFile(path) {
3784
3972
  try {
3785
- return existsSync7(path) && statSync2(path).size > 0;
3973
+ return existsSync7(path) && statSync3(path).size > 0;
3786
3974
  } catch (error) {
3787
3975
  if (error instanceof Error)
3788
3976
  return false;
@@ -3800,7 +3988,7 @@ function hasOtherEvidenceFile(evidenceDir, excludedName) {
3800
3988
  }
3801
3989
  function readCount(counterPath) {
3802
3990
  try {
3803
- const parsed = JSON.parse(readFileSync4(counterPath, "utf8"));
3991
+ const parsed = JSON.parse(readFileSync5(counterPath, "utf8"));
3804
3992
  return typeof parsed["count"] === "number" && parsed["count"] >= 0 ? parsed["count"] : 0;
3805
3993
  } catch (error) {
3806
3994
  if (error instanceof Error)
@@ -3810,7 +3998,7 @@ function readCount(counterPath) {
3810
3998
  }
3811
3999
  function readCounts(counterPath) {
3812
4000
  try {
3813
- const parsed = JSON.parse(readFileSync4(counterPath, "utf8"));
4001
+ const parsed = JSON.parse(readFileSync5(counterPath, "utf8"));
3814
4002
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
3815
4003
  return {};
3816
4004
  const counts = {};
@@ -3827,7 +4015,7 @@ function readCounts(counterPath) {
3827
4015
  }
3828
4016
  function readPlan(goalsPath) {
3829
4017
  try {
3830
- return JSON.parse(readFileSync4(goalsPath, "utf8"));
4018
+ return JSON.parse(readFileSync5(goalsPath, "utf8"));
3831
4019
  } catch (error) {
3832
4020
  if (error instanceof Error)
3833
4021
  return null;
@@ -3836,7 +4024,7 @@ function readPlan(goalsPath) {
3836
4024
  }
3837
4025
 
3838
4026
  // components/ulw-loop/src/stop-resume-hook.ts
3839
- import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
4027
+ import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
3840
4028
  import { isAbsolute as isAbsolute2, join as join4, resolve as resolve4, sep as sep2 } from "node:path";
3841
4029
  var RESUME_CAP = 2;
3842
4030
  var CONTEXT_PRESSURE_MARKERS2 = [
@@ -3856,14 +4044,15 @@ function runStopResumeHook(input) {
3856
4044
  return "";
3857
4045
  if (boulderContinuationWillFire(payload.cwd, payload.session_id))
3858
4046
  return "";
3859
- const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
4047
+ const scope = { sessionId: payload.session_id };
4048
+ const stateDir = ulwLoopDir(payload.cwd, scope);
3860
4049
  const plan = readPlan2(join4(stateDir, "goals.json"));
3861
4050
  if (plan === null || plan.aggregateCompletion?.status === "complete")
3862
4051
  return "";
3863
4052
  const goal3 = resumableGoal(plan);
3864
4053
  if (goal3 === undefined)
3865
4054
  return "";
3866
- if (!consumeResumeBudget(stateDir, goal3.id))
4055
+ if (!consumeResumeBudgetLocked(ulwLoopStateLockPath(payload.cwd, scope), stateDir, goal3.id))
3867
4056
  return "";
3868
4057
  const output = {
3869
4058
  decision: "block",
@@ -3893,6 +4082,15 @@ function resumableGoal(plan) {
3893
4082
  function isResumableStatus(status2) {
3894
4083
  return status2 === "pending" || status2 === "in_progress";
3895
4084
  }
4085
+ function consumeResumeBudgetLocked(lockPath, stateDir, goalId) {
4086
+ try {
4087
+ return withStateLockSync(lockPath, () => consumeResumeBudget(stateDir, goalId));
4088
+ } catch (error) {
4089
+ if (isStateLockTimeout(error))
4090
+ return false;
4091
+ throw error;
4092
+ }
4093
+ }
3896
4094
  function consumeResumeBudget(stateDir, goalId) {
3897
4095
  const ledgerLineCount = countLedgerLines(join4(stateDir, "ledger.jsonl"));
3898
4096
  const counterPath = resolve4(stateDir, `auto-resume-${goalId}.json`);
@@ -3927,7 +4125,7 @@ function renderResumeDirective(plan, goal3, sessionId) {
3927
4125
  }
3928
4126
  function readPlan2(goalsPath) {
3929
4127
  try {
3930
- return JSON.parse(readFileSync5(goalsPath, "utf8"));
4128
+ return JSON.parse(readFileSync6(goalsPath, "utf8"));
3931
4129
  } catch (error) {
3932
4130
  if (error instanceof Error)
3933
4131
  return null;
@@ -3936,7 +4134,7 @@ function readPlan2(goalsPath) {
3936
4134
  }
3937
4135
  function countLedgerLines(ledgerPath) {
3938
4136
  try {
3939
- return readFileSync5(ledgerPath, "utf8").split(`
4137
+ return readFileSync6(ledgerPath, "utf8").split(`
3940
4138
  `).filter(Boolean).length;
3941
4139
  } catch (error) {
3942
4140
  if (error instanceof Error)
@@ -3948,7 +4146,7 @@ function readCounter(counterPath) {
3948
4146
  try {
3949
4147
  if (!existsSync8(counterPath))
3950
4148
  return null;
3951
- const parsed = JSON.parse(readFileSync5(counterPath, "utf8"));
4149
+ const parsed = JSON.parse(readFileSync6(counterPath, "utf8"));
3952
4150
  if (typeof parsed["count"] !== "number" || typeof parsed["ledgerLineCount"] !== "number")
3953
4151
  return null;
3954
4152
  return { count: parsed["count"], ledgerLineCount: parsed["ledgerLineCount"] };
@@ -3960,7 +4158,7 @@ function readCounter(counterPath) {
3960
4158
  }
3961
4159
  function boulderContinuationWillFire(cwd, sessionId) {
3962
4160
  try {
3963
- const raw = JSON.parse(readFileSync5(join4(cwd, ".omo", "boulder.json"), "utf8"));
4161
+ const raw = JSON.parse(readFileSync6(join4(cwd, ".omo", "boulder.json"), "utf8"));
3964
4162
  const works = raw["works"];
3965
4163
  const entries = typeof works === "object" && works !== null ? Object.values(works) : [raw];
3966
4164
  return entries.some((work) => {
@@ -3979,7 +4177,7 @@ function boulderContinuationWillFire(cwd, sessionId) {
3979
4177
  }
3980
4178
  function transcriptShowsContextPressure(transcriptPath) {
3981
4179
  try {
3982
- const transcript = readFileSync5(transcriptPath, "utf8").toLowerCase();
4180
+ const transcript = readFileSync6(transcriptPath, "utf8").toLowerCase();
3983
4181
  return CONTEXT_PRESSURE_MARKERS2.some((marker) => transcript.includes(marker));
3984
4182
  } catch (error) {
3985
4183
  if (error instanceof Error)
@@ -3996,7 +4194,7 @@ function boulderPlanHasChecklist(cwd, entry) {
3996
4194
  const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute2(activePlan) ? [join4(isAbsolute2(worktree) ? worktree : join4(cwd, worktree), activePlan), planPath] : [planPath];
3997
4195
  for (const candidate of candidates) {
3998
4196
  try {
3999
- return readFileSync5(candidate, "utf8").split(/\r?\n/).some((line) => line.startsWith("- [ ] ") || line.startsWith("- [x] ") || line.startsWith("- [X] "));
4197
+ return readFileSync6(candidate, "utf8").split(/\r?\n/).some((line) => line.startsWith("- [ ] ") || line.startsWith("- [x] ") || line.startsWith("- [X] "));
4000
4198
  } catch (error) {
4001
4199
  if (!(error instanceof Error))
4002
4200
  throw error;