lazycodex-ai 4.16.0 → 4.16.2

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 (137) hide show
  1. package/dist/cli/codex-ulw-loop.d.ts +8 -0
  2. package/dist/cli/get-local-version/types.d.ts +1 -1
  3. package/dist/cli/index.js +260 -207
  4. package/dist/cli-node/index.js +260 -207
  5. package/docs/reference/web-terminal-visual-qa.md +39 -45
  6. package/package.json +3 -2
  7. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  8. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  9. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  10. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  11. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  12. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  13. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  14. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  15. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  16. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  17. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  18. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  19. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus.md +1 -1
  20. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  21. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  22. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +3 -3
  23. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  24. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  25. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +1 -1
  26. package/packages/omo-codex/plugin/components/teammode/AGENTS.md +1 -1
  27. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  28. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  29. package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +3 -3
  30. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  31. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  32. package/packages/omo-codex/plugin/components/ultrawork/directive.md +19 -10
  33. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  34. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  35. package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +19 -10
  36. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +13 -0
  37. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +2 -0
  38. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +22 -1
  39. package/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md +4 -0
  40. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +19 -10
  41. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +15 -2
  42. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-steering.js +2 -1
  43. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +89 -27
  44. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +6 -0
  45. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +55 -9
  46. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-snapshot.d.ts +15 -0
  47. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-snapshot.js +33 -0
  48. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-types.d.ts +10 -3
  49. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +15 -11
  50. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
  51. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  52. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +3 -1
  53. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +7 -7
  54. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +17 -2
  55. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-steering.ts +2 -1
  56. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +59 -11
  57. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-snapshot.ts +38 -0
  58. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-types.ts +11 -3
  59. package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +15 -7
  60. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-create-goals.test.ts +16 -0
  61. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +260 -2
  62. package/packages/omo-codex/plugin/components/ulw-loop/test/skill-contract.test.ts +1 -1
  63. package/packages/omo-codex/plugin/components/ulw-loop/test/steering-snapshot.test.ts +124 -0
  64. package/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts +101 -2
  65. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  66. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  67. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  68. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  69. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  70. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  71. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  72. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  73. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  74. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  75. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  76. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  77. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  78. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  79. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  80. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  81. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  82. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  83. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  84. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  85. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  86. package/packages/omo-codex/plugin/package-lock.json +13 -13
  87. package/packages/omo-codex/plugin/package.json +1 -1
  88. package/packages/omo-codex/plugin/scripts/auto-update.mjs +64 -17
  89. package/packages/omo-codex/plugin/scripts/hook-status-message.mjs +10 -6
  90. package/packages/omo-codex/plugin/scripts/migrate-codex-config/multi-agent-v2-guard.mjs +186 -20
  91. package/packages/omo-codex/plugin/scripts/migrate-codex-config/subagent-limit-guard.mjs +51 -3
  92. package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +33 -5
  93. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +1 -1
  94. package/packages/omo-codex/plugin/skills/frontend/SKILL.md +9 -9
  95. package/packages/omo-codex/plugin/skills/frontend/references/design/README.md +7 -3
  96. package/packages/omo-codex/plugin/skills/frontend/references/design/_INDEX.md +1 -1
  97. package/packages/omo-codex/plugin/skills/frontend/references/design/design-system-architecture.md +24 -2
  98. package/packages/omo-codex/plugin/skills/frontend/references/designpowers/README.md +2 -2
  99. package/packages/omo-codex/plugin/skills/frontend/references/designpowers/lane-b-execution.md +1 -1
  100. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +1 -1
  101. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +1 -1
  102. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +1 -1
  103. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +1 -1
  104. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +3 -3
  105. package/packages/omo-codex/plugin/skills/teammode/SKILL.md +3 -3
  106. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +19 -10
  107. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +3 -1
  108. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +7 -7
  109. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +13 -0
  110. package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +2 -0
  111. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +1 -1
  112. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +14 -18
  113. package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +4 -4
  114. package/packages/omo-codex/plugin/test/aggregate-plugin-fixture.mjs +1 -1
  115. package/packages/omo-codex/plugin/test/auto-update.test.mjs +35 -1
  116. package/packages/omo-codex/plugin/test/bootstrap-hooks.test.mjs +1 -1
  117. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +22 -9
  118. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +275 -19
  119. package/packages/omo-codex/plugin/test/subagent-limit-migration.test.mjs +33 -0
  120. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +6 -6
  121. package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +4 -2
  122. package/packages/omo-codex/plugin/test/ulw-plan-skill-contract.test.mjs +52 -0
  123. package/packages/omo-codex/scripts/install-dist/install-local.mjs +65 -39
  124. package/packages/shared-skills/skills/frontend/SKILL.md +9 -9
  125. package/packages/shared-skills/skills/frontend/references/design/README.md +7 -3
  126. package/packages/shared-skills/skills/frontend/references/design/_INDEX.md +1 -1
  127. package/packages/shared-skills/skills/frontend/references/design/design-system-architecture.md +24 -2
  128. package/packages/shared-skills/skills/frontend/references/designpowers/README.md +2 -2
  129. package/packages/shared-skills/skills/frontend/references/designpowers/lane-b-execution.md +1 -1
  130. package/packages/shared-skills/skills/review-work/SKILL.md +3 -1
  131. package/packages/shared-skills/skills/start-work/SKILL.md +4 -2
  132. package/packages/shared-skills/skills/ulw-research/SKILL.md +3 -1
  133. package/packages/shared-skills/skills/visual-qa/SKILL.md +13 -17
  134. package/script/qa/strip-ansi.mjs +10 -0
  135. package/script/qa/web-terminal-visual-qa.mjs +112 -195
  136. package/script/qa/xterm-live-terminal.mjs +180 -0
  137. package/script/qa/web-terminal-renderer.mjs +0 -218
@@ -1,16 +1,18 @@
1
- import { copyFile, mkdir, mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
1
+ import { appendFile, copyFile, mkdir, mkdtemp, readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { setImmediate as tick } from "node:timers/promises";
4
5
  import { beforeEach, describe, expect, it } from "vitest";
5
6
  import { ulwLoopDir, ulwLoopGoalsPath, ulwLoopLedgerPath } from "../src/paths.js";
6
7
  import {
7
8
  appendLedger,
9
+ findAcceptedSteeringLedgerEntry,
8
10
  readSteeringLedgerEntries,
9
11
  readUlwLoopPlan,
10
12
  withUlwLoopMutationLock,
11
13
  writePlan,
12
14
  } from "../src/plan-io.js";
13
- import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan } from "../src/types.js";
15
+ import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSteeringAudit } from "../src/types.js";
14
16
  import { UlwLoopError } from "../src/types.js";
15
17
 
16
18
  const NOW = "2026-05-23T00:00:00.000Z";
@@ -51,6 +53,45 @@ function entry(kind: UlwLoopLedgerEntry["kind"], goalId = "G001"): UlwLoopLedger
51
53
  return { at: NOW, kind, goalId };
52
54
  }
53
55
 
56
+ function steeringAudit(overrides: Partial<UlwLoopSteeringAudit> = {}): UlwLoopSteeringAudit {
57
+ return {
58
+ kind: "revise_pending_wording",
59
+ source: "cli",
60
+ targetGoalIds: ["G001"],
61
+ evidence: "observed evidence",
62
+ rationale: "needed change",
63
+ invariant: {
64
+ accepted: true,
65
+ structuralInvariantAccepted: true,
66
+ evidenceBackedNecessity: true,
67
+ noEasierCompletion: true,
68
+ rejectedReasons: [],
69
+ },
70
+ ...overrides,
71
+ };
72
+ }
73
+
74
+ function rejectedSteeringAudit(overrides: Partial<UlwLoopSteeringAudit> = {}): UlwLoopSteeringAudit {
75
+ return steeringAudit({
76
+ invariant: {
77
+ accepted: false,
78
+ structuralInvariantAccepted: false,
79
+ evidenceBackedNecessity: false,
80
+ noEasierCompletion: true,
81
+ rejectedReasons: ["missing evidence"],
82
+ },
83
+ ...overrides,
84
+ });
85
+ }
86
+
87
+ function steeringEntry(
88
+ kind: UlwLoopLedgerEntry["kind"],
89
+ audit: UlwLoopSteeringAudit,
90
+ overrides: Partial<UlwLoopLedgerEntry> = {},
91
+ ): UlwLoopLedgerEntry {
92
+ return { at: NOW, kind, goalId: "G001", steering: audit, ...overrides };
93
+ }
94
+
54
95
  async function makeRepo(): Promise<string> {
55
96
  return mkdtemp(join(tmpdir(), "ug-io-"));
56
97
  }
@@ -209,6 +250,112 @@ describe("readSteeringLedgerEntries", () => {
209
250
  });
210
251
  });
211
252
 
253
+ describe("findAcceptedSteeringLedgerEntry", () => {
254
+ it.each([
255
+ [
256
+ "top-level idempotencyKey",
257
+ steeringEntry("steering_accepted", steeringAudit(), { idempotencyKey: "needle-key" }),
258
+ ],
259
+ ["steering.idempotencyKey", steeringEntry("steering_accepted", steeringAudit({ idempotencyKey: "needle-key" }))],
260
+ [
261
+ "steering.promptSignature",
262
+ steeringEntry("steering_accepted", steeringAudit({ promptSignature: "needle-key" })),
263
+ ],
264
+ ] as const)("finds an accepted entry by %s", async (_name, target) => {
265
+ // given
266
+ const repoRoot = await makeRepo();
267
+ await appendLedger(repoRoot, steeringEntry("steering_accepted", steeringAudit({ idempotencyKey: "other-key" })));
268
+ await appendLedger(repoRoot, target);
269
+
270
+ // when
271
+ const found = await findAcceptedSteeringLedgerEntry(repoRoot, "needle-key");
272
+
273
+ // then
274
+ expect(found).toEqual(target);
275
+ });
276
+
277
+ it("skips a rejected entry carrying the same key", async () => {
278
+ // given
279
+ const repoRoot = await makeRepo();
280
+ await appendLedger(
281
+ repoRoot,
282
+ steeringEntry("steering_rejected", rejectedSteeringAudit({ idempotencyKey: "needle-key" })),
283
+ );
284
+
285
+ // when/then
286
+ await expect(findAcceptedSteeringLedgerEntry(repoRoot, "needle-key")).resolves.toBeUndefined();
287
+ });
288
+
289
+ it("returns the accepted entry even when a rejected one with the same key precedes it", async () => {
290
+ // given
291
+ const repoRoot = await makeRepo();
292
+ const accepted = steeringEntry("steering_accepted", steeringAudit({ idempotencyKey: "needle-key" }));
293
+ await appendLedger(
294
+ repoRoot,
295
+ steeringEntry("steering_rejected", rejectedSteeringAudit({ idempotencyKey: "needle-key" })),
296
+ );
297
+ await appendLedger(repoRoot, accepted);
298
+
299
+ // when/then
300
+ await expect(findAcceptedSteeringLedgerEntry(repoRoot, "needle-key")).resolves.toEqual(accepted);
301
+ });
302
+
303
+ it("returns undefined when the ledger file is missing", async () => {
304
+ // given
305
+ const repoRoot = await makeRepo();
306
+
307
+ // when/then
308
+ await expect(findAcceptedSteeringLedgerEntry(repoRoot, "needle-key")).resolves.toBeUndefined();
309
+ });
310
+
311
+ it("ignores the key when it only appears in non-steering entries", async () => {
312
+ // given
313
+ const repoRoot = await makeRepo();
314
+ await appendLedger(repoRoot, { at: NOW, kind: "goal_completed", goalId: "G001", idempotencyKey: "needle-key" });
315
+ await appendLedger(repoRoot, { at: NOW, kind: "evidence_captured", goalId: "G001", evidence: "needle-key" });
316
+
317
+ // when/then
318
+ await expect(findAcceptedSteeringLedgerEntry(repoRoot, "needle-key")).resolves.toBeUndefined();
319
+ });
320
+
321
+ it("ignores an accepted entry whose key only appears in evidence text", async () => {
322
+ // given
323
+ const repoRoot = await makeRepo();
324
+ await appendLedger(repoRoot, steeringEntry("steering_accepted", steeringAudit({ evidence: "needle-key" })));
325
+
326
+ // when/then
327
+ await expect(findAcceptedSteeringLedgerEntry(repoRoot, "needle-key")).resolves.toBeUndefined();
328
+ });
329
+
330
+ it("finds the key inside a legacy entry embedding full before/after plans", async () => {
331
+ // given
332
+ const repoRoot = await makeRepo();
333
+ const bulkyGoals = Array.from({ length: 20 }, (_, index) =>
334
+ makeGoal({ id: `G${String(index + 1).padStart(3, "0")}`, objective: `evidence detail ${index} `.repeat(200) }),
335
+ );
336
+ const bulkyPlan = makePlan({ goals: bulkyGoals });
337
+ const legacyLine = JSON.stringify({
338
+ at: NOW,
339
+ kind: "steering_accepted",
340
+ goalId: "G001",
341
+ idempotencyKey: "needle-key",
342
+ before: bulkyPlan,
343
+ after: bulkyPlan,
344
+ steering: { ...steeringAudit({ idempotencyKey: "needle-key" }), before: bulkyPlan, after: bulkyPlan },
345
+ });
346
+ await mkdir(ulwLoopDir(repoRoot), { recursive: true });
347
+ await appendFile(ulwLoopLedgerPath(repoRoot), `${legacyLine}\n`, "utf8");
348
+
349
+ // when
350
+ const found = await findAcceptedSteeringLedgerEntry(repoRoot, "needle-key");
351
+
352
+ // then
353
+ expect(legacyLine.length).toBeGreaterThan(100_000);
354
+ expect(found?.kind).toBe("steering_accepted");
355
+ expect(found?.idempotencyKey).toBe("needle-key");
356
+ });
357
+ });
358
+
212
359
  describe("withUlwLoopMutationLock", () => {
213
360
  it("serializes concurrent invocations", async () => {
214
361
  // given
@@ -236,4 +383,115 @@ describe("withUlwLoopMutationLock", () => {
236
383
  expect(maxActive).toBe(1);
237
384
  expect(await readFile(counterPath, "utf8")).toBe("3");
238
385
  });
386
+
387
+ it("propagates the body rejection and keeps later calls working", async () => {
388
+ // given
389
+ const repoRoot = await makeRepo();
390
+
391
+ // when
392
+ const failure = withUlwLoopMutationLock(repoRoot, async () => {
393
+ throw new Error("body exploded");
394
+ });
395
+
396
+ // then
397
+ await expect(failure).rejects.toThrow("body exploded");
398
+ await expect(withUlwLoopMutationLock(repoRoot, async () => "recovered")).resolves.toBe("recovered");
399
+ });
400
+
401
+ it("still serializes a second batch after the first batch's gate settles", async () => {
402
+ // given
403
+ const repoRoot = await makeRepo();
404
+ let active = 0;
405
+ let maxActive = 0;
406
+ const body = async (): Promise<void> => {
407
+ active += 1;
408
+ maxActive = Math.max(maxActive, active);
409
+ await tick();
410
+ active -= 1;
411
+ };
412
+
413
+ // when
414
+ await Promise.all([1, 2, 3].map(() => withUlwLoopMutationLock(repoRoot, body)));
415
+ await tick();
416
+ await Promise.all([1, 2].map(() => withUlwLoopMutationLock(repoRoot, body)));
417
+
418
+ // then
419
+ expect(maxActive).toBe(1);
420
+ });
421
+
422
+ it("keeps an in-flight successor locked when an earlier gate settles", async () => {
423
+ // given
424
+ const repoRoot = await makeRepo();
425
+ const events: string[] = [];
426
+ let releaseSecond = false;
427
+
428
+ // when
429
+ const first = withUlwLoopMutationLock(repoRoot, async () => {
430
+ events.push("first");
431
+ });
432
+ const second = withUlwLoopMutationLock(repoRoot, async () => {
433
+ events.push("second:start");
434
+ while (!releaseSecond) await tick();
435
+ events.push("second:end");
436
+ });
437
+ await first;
438
+ await tick();
439
+ const third = withUlwLoopMutationLock(repoRoot, async () => {
440
+ events.push("third");
441
+ });
442
+ releaseSecond = true;
443
+ await Promise.all([second, third]);
444
+
445
+ // then
446
+ expect(events).toEqual(["first", "second:start", "second:end", "third"]);
447
+ });
448
+
449
+ it("does not serialize distinct repo roots against each other", async () => {
450
+ // given
451
+ const rootA = await makeRepo();
452
+ const rootB = await makeRepo();
453
+ const events: string[] = [];
454
+ let releaseA = false;
455
+
456
+ // when
457
+ const held = withUlwLoopMutationLock(rootA, async () => {
458
+ events.push("a:start");
459
+ while (!releaseA) await tick();
460
+ events.push("a:end");
461
+ });
462
+ await withUlwLoopMutationLock(rootB, async () => {
463
+ events.push("b");
464
+ });
465
+ releaseA = true;
466
+ await held;
467
+
468
+ // then
469
+ expect(events).toEqual(["a:start", "b", "a:end"]);
470
+ });
471
+
472
+ it("serializes per scope while distinct scopes stay independent", async () => {
473
+ // given
474
+ const repoRoot = await makeRepo();
475
+ const scoped = { sessionId: "session-a" };
476
+ const events: string[] = [];
477
+ let releaseScoped = false;
478
+
479
+ // when
480
+ const heldScoped = withUlwLoopMutationLock(repoRoot, scoped, async () => {
481
+ events.push("scoped-1:start");
482
+ while (!releaseScoped) await tick();
483
+ events.push("scoped-1:end");
484
+ });
485
+ const queuedScoped = withUlwLoopMutationLock(repoRoot, scoped, async () => {
486
+ events.push("scoped-2");
487
+ });
488
+ await withUlwLoopMutationLock(repoRoot, async () => {
489
+ events.push("root-scope");
490
+ });
491
+ releaseScoped = true;
492
+ await Promise.all([heldScoped, queuedScoped]);
493
+
494
+ // then
495
+ expect(events).toEqual(["scoped-1:start", "root-scope", "scoped-1:end", "scoped-2"]);
496
+ });
239
497
  });
@@ -65,6 +65,6 @@ describe("ulw-loop skill contract", () => {
65
65
  // then
66
66
  expect(workflow.match(/malformed input, prompt injection/g)?.length ?? 0).toBe(1);
67
67
  expect(wordCount(workflow)).toBeLessThanOrEqual(3697);
68
- expect(wordCount(skill)).toBeLessThanOrEqual(611);
68
+ expect(wordCount(skill)).toBeLessThanOrEqual(625);
69
69
  });
70
70
  });
@@ -0,0 +1,124 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "../src/steering-snapshot.js";
3
+ import type { UlwLoopItem, UlwLoopPlan } from "../src/types.js";
4
+
5
+ const NOW = "2026-05-23T00:00:00.000Z";
6
+
7
+ function goal(id: string, overrides: Partial<UlwLoopItem> = {}): UlwLoopItem {
8
+ return {
9
+ id,
10
+ title: `Goal ${id}`,
11
+ objective: `Objective for ${id}`,
12
+ status: "pending",
13
+ successCriteria: [],
14
+ attempt: 0,
15
+ createdAt: NOW,
16
+ updatedAt: NOW,
17
+ ...overrides,
18
+ };
19
+ }
20
+
21
+ function plan(goals: UlwLoopItem[], overrides: Partial<UlwLoopPlan> = {}): UlwLoopPlan {
22
+ return {
23
+ version: 1,
24
+ createdAt: NOW,
25
+ updatedAt: NOW,
26
+ briefPath: ".omo/ulw-loop/brief.md",
27
+ goalsPath: ".omo/ulw-loop/goals.json",
28
+ ledgerPath: ".omo/ulw-loop/ledger.jsonl",
29
+ goals,
30
+ ...overrides,
31
+ };
32
+ }
33
+
34
+ describe("changedGoalIdsBetween", () => {
35
+ it.each([
36
+ {
37
+ name: "added goal",
38
+ before: [goal("G001"), goal("G002")],
39
+ after: [goal("G001"), goal("G002"), goal("G003")],
40
+ expected: ["G003"],
41
+ },
42
+ {
43
+ name: "removed goal",
44
+ before: [goal("G001"), goal("G002")],
45
+ after: [goal("G001")],
46
+ expected: ["G002"],
47
+ },
48
+ {
49
+ name: "mutated goal",
50
+ before: [goal("G001"), goal("G002")],
51
+ after: [goal("G001"), goal("G002", { title: "Rewritten" })],
52
+ expected: ["G002"],
53
+ },
54
+ {
55
+ name: "reordered-only goals",
56
+ before: [goal("G001"), goal("G002"), goal("G003")],
57
+ after: [goal("G003"), goal("G001"), goal("G002")],
58
+ expected: [],
59
+ },
60
+ {
61
+ name: "identical plans",
62
+ before: [goal("G001"), goal("G002")],
63
+ after: [goal("G001"), goal("G002")],
64
+ expected: [],
65
+ },
66
+ {
67
+ name: "add + remove + mutate at once",
68
+ before: [goal("G001"), goal("G002"), goal("G003")],
69
+ after: [goal("G001", { status: "blocked" }), goal("G003"), goal("G004")],
70
+ expected: ["G001", "G002", "G004"],
71
+ },
72
+ ])("detects $name", ({ before, after, expected }) => {
73
+ // when
74
+ const changed = changedGoalIdsBetween(plan(before), plan(after));
75
+
76
+ // then
77
+ expect([...changed].sort()).toEqual(expected);
78
+ });
79
+ });
80
+
81
+ describe("buildSteeringPlanSnapshot", () => {
82
+ it("includes only changed goals while keeping full goalIds order and goalCount", () => {
83
+ // given
84
+ const goals = [goal("G001"), goal("G002"), goal("G003"), goal("G004")];
85
+ const source = plan(goals);
86
+
87
+ // when
88
+ const snapshot = buildSteeringPlanSnapshot(source, new Set(["G003"]));
89
+
90
+ // then
91
+ expect(snapshot.goals.map((item) => item.id)).toEqual(["G003"]);
92
+ expect(snapshot.goals[0]).toEqual(goals[2]);
93
+ expect(snapshot.goalIds).toEqual(["G001", "G002", "G003", "G004"]);
94
+ expect(snapshot.goalCount).toBe(4);
95
+ expect(snapshot.updatedAt).toBe(NOW);
96
+ });
97
+
98
+ it("omits the activeGoalId key entirely when the plan has none", () => {
99
+ // when
100
+ const snapshot = buildSteeringPlanSnapshot(plan([goal("G001")]), new Set(["G001"]));
101
+
102
+ // then
103
+ expect("activeGoalId" in snapshot).toBe(false);
104
+ });
105
+
106
+ it("carries activeGoalId when the plan has one", () => {
107
+ // when
108
+ const snapshot = buildSteeringPlanSnapshot(plan([goal("G001")], { activeGoalId: "G001" }), new Set());
109
+
110
+ // then
111
+ expect(snapshot.activeGoalId).toBe("G001");
112
+ });
113
+
114
+ it("keeps plan order for changed goals regardless of set insertion order", () => {
115
+ // given
116
+ const source = plan([goal("G001"), goal("G002"), goal("G003")]);
117
+
118
+ // when
119
+ const snapshot = buildSteeringPlanSnapshot(source, new Set(["G003", "G001"]));
120
+
121
+ // then
122
+ expect(snapshot.goals.map((item) => item.id)).toEqual(["G001", "G003"]);
123
+ });
124
+ });
@@ -1,8 +1,8 @@
1
- import { mkdtemp, readFile } from "node:fs/promises";
1
+ import { appendFile, mkdtemp, readFile } from "node:fs/promises";
2
2
  import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { describe, expect, it } from "vitest";
5
- import { ulwLoopGoalsPath } from "../src/paths.js";
5
+ import { ulwLoopGoalsPath, ulwLoopLedgerPath } from "../src/paths.js";
6
6
  import { readSteeringLedgerEntries, readUlwLoopPlan, writePlan } from "../src/plan-io.js";
7
7
  import {
8
8
  applySteeringMutation,
@@ -12,6 +12,7 @@ import {
12
12
  } from "../src/steering.js";
13
13
  import type {
14
14
  UlwLoopItem,
15
+ UlwLoopLedgerEntry,
15
16
  UlwLoopPlan,
16
17
  UlwLoopSteeringProposal,
17
18
  UlwLoopSuccessCriterion,
@@ -320,6 +321,104 @@ describe("steerUlwLoop", () => {
320
321
  expect(second.deduped).toBe(true);
321
322
  expect((await readUlwLoopPlan(repoRoot)).goals).toHaveLength(4);
322
323
  });
324
+
325
+ describe("compact steering ledger entries", () => {
326
+ const DETAIL = "evidence detail ".repeat(200);
327
+
328
+ function fatGoal(id: string): UlwLoopItem {
329
+ return goal({
330
+ id,
331
+ title: `Fat goal ${id}`,
332
+ objective: `Objective ${id}: ${DETAIL}`,
333
+ evidence: DETAIL,
334
+ successCriteria: [
335
+ criterion({ id: "C001", scenario: DETAIL, expectedEvidence: DETAIL }),
336
+ criterion({ id: "C002", scenario: DETAIL, expectedEvidence: DETAIL }),
337
+ ],
338
+ });
339
+ }
340
+
341
+ function fatPlan(): UlwLoopPlan {
342
+ return plan({
343
+ goals: Array.from({ length: 20 }, (_, index) => fatGoal(`G${String(index + 1).padStart(3, "0")}`)),
344
+ });
345
+ }
346
+
347
+ it("writes an O(changed goals) ledger entry instead of embedding the full plan", async () => {
348
+ // given
349
+ const seed = fatPlan();
350
+ const planBytes = JSON.stringify(seed).length;
351
+ const repoRoot = await repoWithPlan(seed);
352
+
353
+ // when
354
+ await steerUlwLoop(
355
+ repoRoot,
356
+ steering({ kind: "revise_pending_wording", targetGoalId: "G010", revisedTitle: "Sharper goal ten" }),
357
+ );
358
+
359
+ // then
360
+ const lines = (await readFile(ulwLoopLedgerPath(repoRoot), "utf8")).split("\n").filter(Boolean);
361
+ expect(lines).toHaveLength(1);
362
+ const line = lines[0] ?? "";
363
+ expect(line.length).toBeLessThan(planBytes / 4);
364
+ const entry: UlwLoopLedgerEntry = JSON.parse(line);
365
+ expect("before" in entry).toBe(false);
366
+ expect("after" in entry).toBe(false);
367
+ expect(entry.steering?.before?.goals.map((item) => item.id)).toEqual(["G010"]);
368
+ expect(entry.steering?.after?.goals.map((item) => item.id)).toEqual(["G010"]);
369
+ expect(entry.steering?.after?.goals[0]?.title).toBe("Sharper goal ten");
370
+ expect(entry.steering?.before?.goalCount).toBe(20);
371
+ expect(entry.steering?.before?.goalIds).toEqual(seed.goals.map((item) => item.id));
372
+ });
373
+ });
374
+
375
+ it("dedup against a legacy full-plan entry strips before/after from the returned audit", async () => {
376
+ // given
377
+ const seed = plan();
378
+ const repoRoot = await repoWithPlan(seed);
379
+ const legacyLine = JSON.stringify({
380
+ at: NOW,
381
+ kind: "steering_accepted",
382
+ goalId: "G001",
383
+ idempotencyKey: "legacy-key",
384
+ evidence: "observable blocker evidence",
385
+ message: "the plan must change to stay safe",
386
+ mutationKind: "revise_pending_wording",
387
+ steering: {
388
+ kind: "revise_pending_wording",
389
+ source: "cli",
390
+ targetGoalIds: ["G001"],
391
+ evidence: "observable blocker evidence",
392
+ rationale: "the plan must change to stay safe",
393
+ idempotencyKey: "legacy-key",
394
+ invariant: {
395
+ accepted: true,
396
+ structuralInvariantAccepted: true,
397
+ evidenceBackedNecessity: true,
398
+ noEasierCompletion: true,
399
+ rejectedReasons: [],
400
+ },
401
+ before: seed,
402
+ after: seed,
403
+ },
404
+ });
405
+ await appendFile(ulwLoopLedgerPath(repoRoot), `${legacyLine}\n`, "utf8");
406
+
407
+ // when
408
+ const result = await steerUlwLoop(repoRoot, steering({ idempotencyKey: "legacy-key" }));
409
+
410
+ // then
411
+ expect(result.deduped).toBe(true);
412
+ expect(result.accepted).toBe(true);
413
+ expect("before" in result.audit).toBe(false);
414
+ expect("after" in result.audit).toBe(false);
415
+ expect(result.audit).toMatchObject({
416
+ kind: "revise_pending_wording",
417
+ idempotencyKey: "legacy-key",
418
+ deduped: true,
419
+ });
420
+ expect((await readUlwLoopPlan(repoRoot)).goals).toEqual(seed.goals);
421
+ });
323
422
  });
324
423
 
325
424
  describe("parseUlwLoopSteeringDirective", () => {
@@ -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) Resetting Git Bash MCP Reminder",
10
+ "statusMessage": "(OmO 4.16.2) 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) Resetting LSP Diagnostics Cache",
10
+ "statusMessage": "(OmO 4.16.2) 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) Resetting Project Rule Cache",
10
+ "statusMessage": "(OmO 4.16.2) 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/codegraph/dist/cli.js\" hook post-tool-use",
9
9
  "timeout": 5,
10
- "statusMessage": "(OmO) Checking CodeGraph Init Guidance",
10
+ "statusMessage": "(OmO 4.16.2) Checking CodeGraph Init Guidance",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\codegraph\\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/comment-checker/dist/cli.js\" hook post-tool-use",
9
9
  "timeout": 30,
10
- "statusMessage": "(OmO) Checking Comments",
10
+ "statusMessage": "(OmO 4.16.2) 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) Checking LSP Diagnostics",
10
+ "statusMessage": "(OmO 4.16.2) 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) Checking Thread Title Hygiene",
11
+ "statusMessage": "(OmO 4.16.2) 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) Matching Project Rules",
10
+ "statusMessage": "(OmO 4.16.2) 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) Enforcing Unlimited Goal Budget",
10
+ "statusMessage": "(OmO 4.16.2) 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
  ],
@@ -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) Recommending Git Bash MCP",
10
+ "statusMessage": "(OmO 4.16.2) 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": 5,
10
- "statusMessage": "(OmO) Checking Auto Update",
10
+ "statusMessage": "(OmO 4.16.2) 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) Checking Bootstrap Provisioning"
11
+ "statusMessage": "(OmO 4.16.2) Checking Bootstrap Provisioning"
12
12
  }
13
13
  ]
14
14
  }