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
@@ -0,0 +1,106 @@
1
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ import type { UlwLoopPlan } from "../src/types.ts";
7
+
8
+ type EvidenceModule = typeof import("../src/evidence.ts");
9
+
10
+ const NOW = "2026-09-07T00:00:00.000Z";
11
+ const SCOPE = { sessionId: "s1" } as const;
12
+ const CRITERIA = ["C001", "C002", "C003"] as const;
13
+
14
+ let repoRoot: string;
15
+
16
+ beforeEach(async () => {
17
+ repoRoot = await mkdtemp(join(tmpdir(), "ulw-cross-process-"));
18
+ await mkdir(join(repoRoot, ".omo", "ulw-loop", "s1"), { recursive: true });
19
+ await writeFile(join(repoRoot, ".omo", "ulw-loop", "s1", "goals.json"), `${JSON.stringify(plan(), null, 2)}\n`);
20
+ await writeFile(join(repoRoot, ".omo", "ulw-loop", "s1", "ledger.jsonl"), "");
21
+ });
22
+
23
+ afterEach(async () => {
24
+ vi.resetModules();
25
+ await rm(repoRoot, { recursive: true, force: true });
26
+ });
27
+
28
+ function plan(): UlwLoopPlan {
29
+ return {
30
+ version: 1,
31
+ evidenceLayoutVersion: 2,
32
+ createdAt: NOW,
33
+ updatedAt: NOW,
34
+ briefPath: ".omo/ulw-loop/s1/brief.md",
35
+ goalsPath: ".omo/ulw-loop/s1/goals.json",
36
+ ledgerPath: ".omo/ulw-loop/s1/ledger.jsonl",
37
+ codexGoalMode: "aggregate",
38
+ activeGoalId: "G001",
39
+ goals: [
40
+ {
41
+ id: "G001",
42
+ title: "Goal one",
43
+ objective: "Goal one",
44
+ status: "in_progress",
45
+ attempt: 1,
46
+ createdAt: NOW,
47
+ updatedAt: NOW,
48
+ successCriteria: CRITERIA.map((id) => ({
49
+ id,
50
+ scenario: `scenario ${id}`,
51
+ userModel: "happy",
52
+ expectedEvidence: "evidence",
53
+ essential: true,
54
+ capturedEvidence: null,
55
+ status: "pending",
56
+ })),
57
+ },
58
+ ],
59
+ };
60
+ }
61
+
62
+ // Each isolated import evaluates plan-io again, so every instance owns a fresh
63
+ // in-process promise chain: the same situation as separate CLI processes
64
+ // sharing one state directory.
65
+ async function loadIsolatedEvidenceModule(): Promise<EvidenceModule> {
66
+ vi.resetModules();
67
+ return await import("../src/evidence.ts");
68
+ }
69
+
70
+ async function readPlanFile(): Promise<UlwLoopPlan> {
71
+ return JSON.parse(await readFile(join(repoRoot, ".omo", "ulw-loop", "s1", "goals.json"), "utf8"));
72
+ }
73
+
74
+ async function readLedgerKinds(): Promise<readonly string[]> {
75
+ const raw = await readFile(join(repoRoot, ".omo", "ulw-loop", "s1", "ledger.jsonl"), "utf8");
76
+ return raw
77
+ .split("\n")
78
+ .filter((line) => line.trim().length > 0)
79
+ .map((line) => JSON.parse(line).kind);
80
+ }
81
+
82
+ describe("record-evidence across independent module instances", () => {
83
+ describe("#given three evidence writers that share only the state directory", () => {
84
+ it("#when they record different criteria concurrently #then every criterion lands in goals.json", async () => {
85
+ const writers: { readonly criterionId: string; readonly module: EvidenceModule }[] = [];
86
+ for (const criterionId of CRITERIA) {
87
+ writers.push({ criterionId, module: await loadIsolatedEvidenceModule() });
88
+ }
89
+
90
+ await Promise.all(
91
+ writers.map(({ criterionId, module }) =>
92
+ module.recordEvidence(
93
+ repoRoot,
94
+ { goalId: "G001", criterionId, status: "pass", evidence: `proof ${criterionId}` },
95
+ SCOPE,
96
+ ),
97
+ ),
98
+ );
99
+
100
+ const persisted = await readPlanFile();
101
+ const statuses = persisted.goals[0]?.successCriteria.map((criterion) => criterion.status);
102
+ expect(statuses).toEqual(["pass", "pass", "pass"]);
103
+ expect(await readLedgerKinds()).toEqual(["evidence_captured", "evidence_captured", "evidence_captured"]);
104
+ });
105
+ });
106
+ });
@@ -1,3 +1,4 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { appendFile, copyFile, mkdir, mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
2
3
  import { tmpdir } from "node:os";
3
4
  import { join } from "node:path";
@@ -140,8 +141,8 @@ describe("readUlwLoopPlan", () => {
140
141
  const legacyObjective = "Complete all ulw-loop stories in .omo/ulw-loop/goals.json: G001 Build auth service";
141
142
  await writeRawPlan(repoRoot, makePlan({ codexObjective: legacyObjective }));
142
143
 
143
- // when
144
- const plan = await readUlwLoopPlan(repoRoot);
144
+ // when: read inside the mutation lock, the way every mutating command does
145
+ const plan = await withUlwLoopMutationLock(repoRoot, () => readUlwLoopPlan(repoRoot));
145
146
 
146
147
  // then
147
148
  expect(plan.codexObjective).toBe(STABLE_OBJECTIVE);
@@ -155,6 +156,39 @@ describe("readUlwLoopPlan", () => {
155
156
  before: { codexObjective: legacyObjective },
156
157
  });
157
158
  });
159
+
160
+ it("readUlwLoopPlan refuses to migrate a legacy aggregate plan outside the mutation lock (no unlocked write on a read path)", async () => {
161
+ // given: the same legacy plan, but the read is not wrapped in withUlwLoopMutationLock
162
+ const legacyObjective = "Complete all ulw-loop stories in .omo/ulw-loop/goals.json: G001 Build auth service";
163
+ await writeRawPlan(repoRoot, makePlan({ codexObjective: legacyObjective }));
164
+
165
+ // when
166
+ let caught: unknown;
167
+ try {
168
+ await readUlwLoopPlan(repoRoot);
169
+ } catch (error) {
170
+ caught = error;
171
+ }
172
+
173
+ // then: it refuses with a recovery hint and writes nothing
174
+ expect(caught).toBeInstanceOf(UlwLoopError);
175
+ expect((caught as UlwLoopError).code).toBe("ULW_LOOP_MIGRATION_REQUIRED");
176
+ const persisted = JSON.parse(await readFile(ulwLoopGoalsPath(repoRoot), "utf8"));
177
+ expect(persisted.codexObjective).toBe(legacyObjective);
178
+ expect(existsSync(ulwLoopLedgerPath(repoRoot))).toBe(false);
179
+ });
180
+
181
+ it("a read of an already-migrated plan outside the lock still succeeds", async () => {
182
+ // given
183
+ await writeRawPlan(repoRoot, makePlan({ codexObjective: STABLE_OBJECTIVE }));
184
+
185
+ // when
186
+ const plan = await readUlwLoopPlan(repoRoot);
187
+
188
+ // then
189
+ expect(plan.codexObjective).toBe(STABLE_OBJECTIVE);
190
+ expect(existsSync(ulwLoopLedgerPath(repoRoot))).toBe(false);
191
+ });
158
192
  });
159
193
 
160
194
  describe("writePlan", () => {
@@ -543,3 +543,40 @@ describe("applySpawnGuards gate-artifact check before reviewer quota", () => {
543
543
  expect(counters["lazycodex-gate-reviewer:g1:a1"]).toBe(1);
544
544
  });
545
545
  });
546
+
547
+ describe("applySpawnGuards session state lock", () => {
548
+ function lockPath(): string {
549
+ return join(sessionDir(), ".state.lock");
550
+ }
551
+
552
+ it("#given the state lock held by a live process past the timeout #when guarded #then denies without counting", () => {
553
+ writeGoals();
554
+ mkdirSync(sessionDir(), { recursive: true });
555
+ writeFileSync(
556
+ lockPath(),
557
+ JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), token: "live" }),
558
+ );
559
+
560
+ const output = applySpawnGuards(payload("spawn_agent", { message: "scan" }), { lockTimeoutMs: 100 });
561
+
562
+ expect(deny(output).permissionDecision).toBe("deny");
563
+ expect(deny(output).permissionDecisionReason).toContain("state lock");
564
+ expect(existsSync(join(sessionDir(), "spawn-count.json"))).toBe(false);
565
+ expect(existsSync(lockPath())).toBe(true);
566
+ });
567
+
568
+ it("#given a stale lock from a dead process #when guarded #then reclaims it and counts normally", () => {
569
+ writeGoals();
570
+ mkdirSync(sessionDir(), { recursive: true });
571
+ writeFileSync(
572
+ lockPath(),
573
+ JSON.stringify({ pid: 2147483646, createdAt: "2026-01-01T00:00:00.000Z", token: "gone" }),
574
+ );
575
+
576
+ expect(applySpawnGuards(payload("spawn_agent", { message: "scan" }), { lockTimeoutMs: 1000 })).toBe("");
577
+
578
+ const counter = JSON.parse(readFileSync(join(sessionDir(), "spawn-count.json"), "utf8"));
579
+ expect(counter.count).toBe(1);
580
+ expect(existsSync(lockPath())).toBe(false);
581
+ });
582
+ });
@@ -0,0 +1,218 @@
1
+ import { type ChildProcess, spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
3
+ import { mkdtemp, rm, utimes, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
7
+
8
+ import { ULW_LOOP_LOCK_TIMEOUT_CODE, withStateLock, withStateLockSync } from "../src/state-lock.ts";
9
+ import { UlwLoopError } from "../src/types.ts";
10
+
11
+ let workDir: string;
12
+ let lockPath: string;
13
+ const children: ChildProcess[] = [];
14
+
15
+ beforeEach(async () => {
16
+ workDir = await mkdtemp(join(tmpdir(), "ulw-state-lock-"));
17
+ lockPath = join(workDir, ".omo", "ulw-loop", "s1", ".state.lock");
18
+ });
19
+
20
+ afterEach(async () => {
21
+ for (const child of children.splice(0)) {
22
+ if (child.exitCode === null) child.kill("SIGKILL");
23
+ }
24
+ await rm(workDir, { recursive: true, force: true });
25
+ });
26
+
27
+ const HOLDER_SCRIPT = `
28
+ const fs = require("node:fs");
29
+ const [lockPath, holdMs] = process.argv.slice(1);
30
+ fs.mkdirSync(require("node:path").dirname(lockPath), { recursive: true });
31
+ const fd = fs.openSync(lockPath, "wx");
32
+ fs.writeSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), token: "holder-" + process.pid }));
33
+ fs.closeSync(fd);
34
+ process.stdout.write("HELD\\n");
35
+ setTimeout(() => {
36
+ fs.writeFileSync(lockPath + ".released", "");
37
+ fs.unlinkSync(lockPath);
38
+ }, Number(holdMs));
39
+ `;
40
+
41
+ interface ForeignHolder {
42
+ readonly released: () => boolean;
43
+ }
44
+
45
+ // A real second process holds the lock file the way another CLI invocation
46
+ // would. Release is observed through a marker file so the check also works
47
+ // while the synchronous acquirer has the event loop blocked.
48
+ function spawnForeignHolder(holdMs: number): Promise<ForeignHolder> {
49
+ return new Promise((resolve, reject) => {
50
+ const child = spawn(process.execPath, ["-e", HOLDER_SCRIPT, lockPath, String(holdMs)], {
51
+ stdio: ["ignore", "pipe", "inherit"],
52
+ });
53
+ children.push(child);
54
+ let output = "";
55
+ child.stdout.setEncoding("utf8");
56
+ child.stdout.on("data", (chunk: string) => {
57
+ output += chunk;
58
+ if (output.includes("HELD")) resolve({ released: () => existsSync(`${lockPath}.released`) });
59
+ });
60
+ child.once("error", reject);
61
+ });
62
+ }
63
+
64
+ function spawnExitedProcess(): Promise<number> {
65
+ return new Promise((resolve, reject) => {
66
+ const child = spawn(process.execPath, ["-e", "process.exit(0)"], { stdio: "ignore" });
67
+ child.once("exit", () => {
68
+ if (child.pid === undefined) reject(new Error("child had no pid"));
69
+ else resolve(child.pid);
70
+ });
71
+ child.once("error", reject);
72
+ });
73
+ }
74
+
75
+ function ownerPid(): number {
76
+ const record: unknown = JSON.parse(readFileSync(lockPath, "utf8"));
77
+ if (typeof record !== "object" || record === null || !("pid" in record) || typeof record.pid !== "number") {
78
+ throw new Error("lock record has no pid");
79
+ }
80
+ return record.pid;
81
+ }
82
+
83
+ describe("withStateLock", () => {
84
+ describe("#given a lock held by another live process", () => {
85
+ it("#when acquiring #then it waits for that process to release before running the body", async () => {
86
+ const holder = await spawnForeignHolder(300);
87
+
88
+ const observedReleaseFirst = await withStateLock(lockPath, async () => holder.released(), {
89
+ timeoutMs: 5_000,
90
+ });
91
+
92
+ expect(observedReleaseFirst).toBe(true);
93
+ expect(existsSync(lockPath)).toBe(false);
94
+ });
95
+
96
+ it("#when the holder outlives the timeout #then it fails closed without running the body", async () => {
97
+ await spawnForeignHolder(5_000);
98
+ let bodyRan = false;
99
+
100
+ const attempt = withStateLock(
101
+ lockPath,
102
+ async () => {
103
+ bodyRan = true;
104
+ },
105
+ { timeoutMs: 150 },
106
+ );
107
+
108
+ await expect(attempt).rejects.toMatchObject({ code: ULW_LOOP_LOCK_TIMEOUT_CODE });
109
+ await expect(attempt).rejects.toBeInstanceOf(UlwLoopError);
110
+ expect(bodyRan).toBe(false);
111
+ expect(existsSync(lockPath)).toBe(true);
112
+ });
113
+ });
114
+
115
+ describe("#given a lock left behind by a process that is gone", () => {
116
+ it("#when acquiring #then the stale lock is reclaimed immediately", async () => {
117
+ const deadPid = await spawnExitedProcess();
118
+ mkdirSync(join(workDir, ".omo", "ulw-loop", "s1"), { recursive: true });
119
+ await writeFile(
120
+ lockPath,
121
+ JSON.stringify({ pid: deadPid, createdAt: new Date().toISOString(), token: "gone" }),
122
+ );
123
+
124
+ const seenOwner = await withStateLock(lockPath, async () => ownerPid(), { timeoutMs: 1_000 });
125
+
126
+ expect(seenOwner).toBe(process.pid);
127
+ });
128
+
129
+ it("#when a legacy record without a token is young #then it is treated as mid-write and waited on, not stolen", async () => {
130
+ mkdirSync(join(workDir, ".omo", "ulw-loop", "s1"), { recursive: true });
131
+ await writeFile(lockPath, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }));
132
+
133
+ const attempt = withStateLock(lockPath, async () => "unreachable", { timeoutMs: 150, staleMs: 60_000 });
134
+
135
+ await expect(attempt).rejects.toMatchObject({ code: ULW_LOOP_LOCK_TIMEOUT_CODE });
136
+ expect(existsSync(lockPath)).toBe(true);
137
+ });
138
+ });
139
+
140
+ describe("#given a live holder whose lock is older than staleMs", () => {
141
+ it("#when acquiring #then age alone never reclaims it and the waiter fails closed", async () => {
142
+ await spawnForeignHolder(5_000);
143
+ const holderPid = ownerPid();
144
+ const twoMinutesAgo = new Date(Date.now() - 120_000);
145
+ await utimes(lockPath, twoMinutesAgo, twoMinutesAgo);
146
+ let bodyRan = false;
147
+
148
+ const attempt = withStateLock(
149
+ lockPath,
150
+ async () => {
151
+ bodyRan = true;
152
+ },
153
+ { timeoutMs: 200, staleMs: 60_000 },
154
+ );
155
+
156
+ await expect(attempt).rejects.toMatchObject({ code: ULW_LOOP_LOCK_TIMEOUT_CODE });
157
+ expect(bodyRan).toBe(false);
158
+ expect(ownerPid()).toBe(holderPid);
159
+ });
160
+ });
161
+
162
+ describe("#given the lock changed hands while the body ran", () => {
163
+ it("#when the original owner releases #then it leaves the successor's lock in place", async () => {
164
+ const foreign = JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), token: "successor" });
165
+
166
+ await withStateLock(lockPath, async () => {
167
+ await writeFile(lockPath, foreign);
168
+ });
169
+
170
+ expect(existsSync(lockPath)).toBe(true);
171
+ expect(readFileSync(lockPath, "utf8")).toBe(foreign);
172
+ });
173
+
174
+ it("#when the record is unreadable but older than staleMs #then it is reclaimed", async () => {
175
+ mkdirSync(join(workDir, ".omo", "ulw-loop", "s1"), { recursive: true });
176
+ await writeFile(lockPath, "not json");
177
+ const twoMinutesAgo = new Date(Date.now() - 120_000);
178
+ await utimes(lockPath, twoMinutesAgo, twoMinutesAgo);
179
+
180
+ const seenOwner = await withStateLock(lockPath, async () => ownerPid(), {
181
+ timeoutMs: 1_000,
182
+ staleMs: 60_000,
183
+ });
184
+
185
+ expect(seenOwner).toBe(process.pid);
186
+ });
187
+ });
188
+
189
+ describe("#given the body throws", () => {
190
+ it("#when it rejects #then the lock is released and the error propagates", async () => {
191
+ const failure = withStateLock(lockPath, async () => {
192
+ throw new Error("body exploded");
193
+ });
194
+
195
+ await expect(failure).rejects.toThrow("body exploded");
196
+ expect(existsSync(lockPath)).toBe(false);
197
+ });
198
+ });
199
+ });
200
+
201
+ describe("withStateLockSync", () => {
202
+ it("#given a lock held by another live process #when acquiring #then it blocks until release", async () => {
203
+ const holder = await spawnForeignHolder(300);
204
+
205
+ const observedReleaseFirst = withStateLockSync(lockPath, () => holder.released(), { timeoutMs: 5_000 });
206
+
207
+ expect(observedReleaseFirst).toBe(true);
208
+ expect(existsSync(lockPath)).toBe(false);
209
+ });
210
+
211
+ it("#given the holder outlives the timeout #when acquiring #then it throws the timeout error", async () => {
212
+ await spawnForeignHolder(5_000);
213
+
214
+ expect(() => withStateLockSync(lockPath, () => "unreachable", { timeoutMs: 150 })).toThrow(
215
+ expect.objectContaining({ code: ULW_LOOP_LOCK_TIMEOUT_CODE }),
216
+ );
217
+ });
218
+ });
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/git-bash/dist/cli.js\" hook post-compact",
9
9
  "timeout": 5,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Resetting Git Bash MCP Reminder",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Resetting Git Bash MCP Reminder",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\git-bash\\dist\\cli.js\" hook post-compact"
12
12
  }
13
13
  ],
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/lsp/dist/cli.js\" hook post-compact",
9
9
  "timeout": 5,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Resetting LSP Diagnostics Cache",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Resetting LSP Diagnostics Cache",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\lsp\\dist\\cli.js\" hook post-compact"
12
12
  }
13
13
  ],
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/rules/dist/cli.js\" hook post-compact",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Resetting Project Rule Cache",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Resetting Project Rule Cache",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\rules\\dist\\cli.js\" hook post-compact"
12
12
  }
13
13
  ],
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/comment-checker/dist/cli.js\" hook post-tool-use",
9
9
  "timeout": 30,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Checking Comments",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Checking Comments",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\comment-checker\\dist\\cli.js\" hook post-tool-use"
12
12
  }
13
13
  ],
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/lsp/dist/cli.js\" hook post-tool-use",
9
9
  "timeout": 60,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Checking LSP Diagnostics",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Checking LSP Diagnostics",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\lsp\\dist\\cli.js\" hook post-tool-use"
12
12
  }
13
13
  ],
@@ -8,7 +8,7 @@
8
8
  "type": "command",
9
9
  "command": "node \"${PLUGIN_ROOT}/components/teammode/dist/cli.js\" hook post-tool-use",
10
10
  "timeout": 10,
11
- "statusMessage": "(OmO 5.0.0-beta.45) Checking Thread Title Hygiene",
11
+ "statusMessage": "(OmO 5.0.0-beta.47) Checking Thread Title Hygiene",
12
12
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\teammode\\dist\\cli.js\" hook post-tool-use"
13
13
  }
14
14
  ]
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/rules/dist/cli.js\" hook post-tool-use",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Matching Project Rules",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Matching Project Rules",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\rules\\dist\\cli.js\" hook post-tool-use"
12
12
  }
13
13
  ],
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/ulw-loop/dist/cli.js\" hook pre-tool-use",
9
9
  "timeout": 5,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Enforcing Unlimited Goal Budget",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Enforcing Unlimited Goal Budget",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\ulw-loop\\dist\\cli.js\" hook pre-tool-use"
12
12
  }
13
13
  ],
@@ -8,7 +8,7 @@
8
8
  "type": "command",
9
9
  "command": "node \"${PLUGIN_ROOT}/components/ulw-loop/dist/cli.js\" hook pre-tool-use-spawn",
10
10
  "timeout": 5,
11
- "statusMessage": "(OmO 5.0.0-beta.45) Guarding Ulw-Loop Spawns",
11
+ "statusMessage": "(OmO 5.0.0-beta.47) Guarding Ulw-Loop Spawns",
12
12
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\ulw-loop\\dist\\cli.js\" hook pre-tool-use-spawn"
13
13
  }
14
14
  ]
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/git-bash/dist/cli.js\" hook pre-tool-use",
9
9
  "timeout": 5,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Recommending Git Bash MCP",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Recommending Git Bash MCP",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\git-bash\\dist\\cli.js\" hook pre-tool-use"
12
12
  }
13
13
  ],
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/scripts/auto-update.mjs\" hook session-start",
9
9
  "timeout": 15,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Checking Auto Update",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Checking Auto Update",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\scripts\\auto-update.mjs\" hook session-start"
12
12
  }
13
13
  ],
@@ -8,7 +8,7 @@
8
8
  "command": "node \"${PLUGIN_ROOT}/components/bootstrap/dist/cli.js\" hook session-start",
9
9
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\bootstrap.ps1\"",
10
10
  "timeout": 30,
11
- "statusMessage": "(OmO 5.0.0-beta.45) Checking Bootstrap Provisioning"
11
+ "statusMessage": "(OmO 5.0.0-beta.47) Checking Bootstrap Provisioning"
12
12
  }
13
13
  ]
14
14
  }
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/rules/dist/cli.js\" hook session-start",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Loading Project Rules",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Loading Project Rules",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\rules\\dist\\cli.js\" hook session-start"
12
12
  }
13
13
  ]
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/telemetry/dist/cli.js\" hook session-start",
9
9
  "timeout": 15,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Recording Session Telemetry",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Recording Session Telemetry",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\telemetry\\dist\\cli.js\" hook session-start"
12
12
  }
13
13
  ]
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/ulw-execute-continuation/dist/cli.js\" hook stop",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Checking Ulw-Execute Continuation",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Checking Ulw-Execute Continuation",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\ulw-execute-continuation\\dist\\cli.js\" hook stop"
12
12
  }
13
13
  ]
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/ulw-loop/dist/cli.js\" hook stop",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Checking Ulw-Loop Resume",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Checking Ulw-Loop Resume",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\ulw-loop\\dist\\cli.js\" hook stop"
12
12
  }
13
13
  ]
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/ulw-execute-continuation/dist/cli.js\" hook subagent-stop",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Checking Ulw-Execute Continuation",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Checking Ulw-Execute Continuation",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\ulw-execute-continuation\\dist\\cli.js\" hook subagent-stop"
12
12
  }
13
13
  ]
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/lazycodex-executor-verify/dist/cli.js\" hook subagent-stop",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Verifying LazyCodex Executor Evidence",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Verifying LazyCodex Executor Evidence",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\lazycodex-executor-verify\\dist\\cli.js\" hook subagent-stop"
12
12
  }
13
13
  ],
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/ultrawork/dist/cli.js\" hook user-prompt-submit",
9
9
  "timeout": 5,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Checking Ultrawork Trigger",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Checking Ultrawork Trigger",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\ultrawork\\dist\\cli.js\" hook user-prompt-submit"
12
12
  }
13
13
  ]
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/ulw-loop/dist/cli.js\" hook user-prompt-submit",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Checking Ulw-Loop Steering",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Checking Ulw-Loop Steering",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\ulw-loop\\dist\\cli.js\" hook user-prompt-submit"
12
12
  }
13
13
  ]
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/rules/dist/cli.js\" hook user-prompt-submit",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.45) Loading Project Rules",
10
+ "statusMessage": "(OmO 5.0.0-beta.47) Loading Project Rules",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\rules\\dist\\cli.js\" hook user-prompt-submit"
12
12
  }
13
13
  ]