lazycodex-ai 5.0.0-beta.46 → 5.0.0-beta.48

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 (105) hide show
  1. package/dist/cli/index.js +15 -15
  2. package/dist/cli-node/index.js +15 -15
  3. package/package.json +1 -1
  4. package/packages/lsp-daemon/dist/cli.js +6 -3
  5. package/packages/lsp-daemon/dist/client.js +6 -3
  6. package/packages/lsp-daemon/dist/ensure-daemon.d.ts +6 -1
  7. package/packages/lsp-daemon/dist/ensure-daemon.js +9 -2
  8. package/packages/lsp-daemon/dist/index.js +6 -3
  9. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  10. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  11. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  12. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  13. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  14. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  15. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  16. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  17. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  18. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  19. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +6 -3
  20. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  21. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  22. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  23. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  25. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  26. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  27. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  28. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  29. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  30. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
  31. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  32. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-commands.js +15 -7
  33. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -1
  34. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +1 -1
  35. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +294 -96
  36. package/packages/omo-codex/plugin/components/ulw-loop/dist/constants.d.ts +1 -0
  37. package/packages/omo-codex/plugin/components/ulw-loop/dist/constants.js +1 -0
  38. package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.d.ts +1 -0
  39. package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.js +12 -3
  40. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.d.ts +1 -0
  41. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-io.js +21 -6
  42. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.d.ts +1 -0
  43. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-missing-recovery.js +10 -0
  44. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.d.ts +4 -1
  45. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +25 -57
  46. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.d.ts +9 -0
  47. package/packages/omo-codex/plugin/components/ulw-loop/dist/state-lock.js +227 -0
  48. package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +17 -3
  49. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  50. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  51. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +2 -1
  52. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +10 -7
  53. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-commands.ts +18 -7
  54. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +1 -1
  55. package/packages/omo-codex/plugin/components/ulw-loop/src/constants.ts +1 -0
  56. package/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts +24 -3
  57. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts +25 -5
  58. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-missing-recovery.ts +11 -0
  59. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +34 -64
  60. package/packages/omo-codex/plugin/components/ulw-loop/src/state-lock.ts +239 -0
  61. package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +16 -3
  62. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-template.test.ts +14 -4
  63. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-checkpoint-continuation.test.ts +4 -2
  64. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-checkpoint.test.ts +4 -2
  65. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-commands.test.ts +2 -0
  66. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-complete-goals.test.ts +3 -1
  67. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-create-goals.test.ts +10 -8
  68. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-entrypoint.test.ts +12 -2
  69. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-json-errors.test.ts +2 -0
  70. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-scope-required.test.ts +144 -0
  71. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-status-next-actions.test.ts +7 -5
  72. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-steering-batch.test.ts +8 -3
  73. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-validation-batch.test.ts +6 -0
  74. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/cli-session.ts +4 -0
  75. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/quality-gate-builder.ts +2 -1
  76. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io-cross-process.test.ts +106 -0
  77. package/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts +36 -2
  78. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +37 -0
  79. package/packages/omo-codex/plugin/components/ulw-loop/test/state-lock.test.ts +218 -0
  80. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  81. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  82. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  83. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  84. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  85. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  86. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  87. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  88. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  89. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  90. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  91. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  92. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  93. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  94. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  95. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  96. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
  97. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  98. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  99. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  100. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  101. package/packages/omo-codex/plugin/package-lock.json +12 -12
  102. package/packages/omo-codex/plugin/package.json +1 -1
  103. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +2 -1
  104. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +10 -7
  105. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
@@ -8,9 +8,11 @@ import { ulwLoopCommand } from "../src/cli-commands.js";
8
8
  import { writePlan } from "../src/plan-io.js";
9
9
  import { validateQualityGate } from "../src/quality-gate.js";
10
10
  import { goal, plan } from "./fixtures/checkpoint-builders.js";
11
+ import { CLI_TEST_SCOPE, CLI_TEST_SESSION_ID } from "./fixtures/cli-session.js";
11
12
 
12
13
  let repo: string;
13
14
  let output: string[];
15
+ let originalOmoSessionId: string | undefined;
14
16
 
15
17
  beforeEach(async () => {
16
18
  repo = await mkdtemp(join(tmpdir(), "ulw-template-"));
@@ -21,10 +23,14 @@ beforeEach(async () => {
21
23
  return true;
22
24
  });
23
25
  delete process.env["OMO_AGENT_TOOLKIT_SURFACE"];
26
+ originalOmoSessionId = process.env["OMO_ULW_LOOP_SESSION_ID"];
27
+ process.env["OMO_ULW_LOOP_SESSION_ID"] = CLI_TEST_SESSION_ID;
24
28
  });
25
29
 
26
30
  afterEach(async () => {
27
31
  vi.restoreAllMocks();
32
+ if (originalOmoSessionId === undefined) delete process.env["OMO_ULW_LOOP_SESSION_ID"];
33
+ else process.env["OMO_ULW_LOOP_SESSION_ID"] = originalOmoSessionId;
28
34
  await rm(repo, { recursive: true, force: true });
29
35
  });
30
36
 
@@ -54,6 +60,7 @@ describe("checkpoint --print-template", () => {
54
60
  activeGoalId: "G001",
55
61
  codexObjective: "Exact objective from goals.json",
56
62
  }),
63
+ CLI_TEST_SCOPE,
57
64
  );
58
65
  process.env["OMO_AGENT_TOOLKIT_SURFACE"] = "omo-senpi";
59
66
  const result = await run(["--print-template", "--goal-id", "G001", "--json"]);
@@ -86,6 +93,7 @@ describe("checkpoint --print-template", () => {
86
93
  activeGoalId: "G001",
87
94
  codexObjective: "Exact objective from goals.json",
88
95
  }),
96
+ CLI_TEST_SCOPE,
89
97
  );
90
98
  process.env["OMO_AGENT_TOOLKIT_SURFACE"] = "omo-senpi";
91
99
  const result = await run(["--print-template", "--goal-id", "G001", "--json"]);
@@ -96,7 +104,7 @@ describe("checkpoint --print-template", () => {
96
104
  expect(result["codexGoalTemplate"]).toEqual({
97
105
  goal: { objective: "Exact objective from goals.json", status: "complete" },
98
106
  });
99
- expect(result["attemptDir"]).toBe(".omo/evidence/ulw/session/G001/a2");
107
+ expect(result["attemptDir"]).toBe(".omo/evidence/ulw/cli-test/G001/a2");
100
108
  });
101
109
 
102
110
  it("#given an active v2 plan #when printed without any goal id #then targets the active goal", async () => {
@@ -107,12 +115,13 @@ describe("checkpoint --print-template", () => {
107
115
  activeGoalId: "G001",
108
116
  codexObjective: "Exact objective from goals.json",
109
117
  }),
118
+ CLI_TEST_SCOPE,
110
119
  );
111
120
  process.env["OMO_AGENT_TOOLKIT_SURFACE"] = "omo-senpi";
112
121
  const result = await run(["--print-template", "--json"]);
113
122
  const gate = result["qualityGateTemplate"] as Record<string, unknown>;
114
123
  expect(Object.keys(gate)).toEqual(["manualQa", "gateReview", "iteration", "criteriaCoverage"]);
115
- expect(result["attemptDir"]).toBe(".omo/evidence/ulw/session/G001/a1");
124
+ expect(result["attemptDir"]).toBe(".omo/evidence/ulw/cli-test/G001/a1");
116
125
  });
117
126
 
118
127
  it("#given two goals #when printed with an explicit non-active goal id #then targets that goal's attempt dir", async () => {
@@ -123,13 +132,14 @@ describe("checkpoint --print-template", () => {
123
132
  activeGoalId: "G001",
124
133
  codexObjective: "Exact objective from goals.json",
125
134
  }),
135
+ CLI_TEST_SCOPE,
126
136
  );
127
137
  const result = await run(["--print-template", "--goal-id", "G002", "--json"]);
128
- expect(result["attemptDir"]).toBe(".omo/evidence/ulw/session/G002/a3");
138
+ expect(result["attemptDir"]).toBe(".omo/evidence/ulw/cli-test/G002/a3");
129
139
  });
130
140
 
131
141
  it("#given a v1 plan #when printed without status #then exits successfully and gives v1 guidance", async () => {
132
- await writePlan(repo, plan([goal({ id: "G001" })]));
142
+ await writePlan(repo, plan([goal({ id: "G001" })]), CLI_TEST_SCOPE);
133
143
  const result = await run(["--print-template", "--goal-id", "G001", "--json"]);
134
144
  expect(result["guidance"]).toContain("evidence-layout v1");
135
145
  expect(result).not.toHaveProperty("attemptDir");
@@ -4,7 +4,8 @@ import { join } from "node:path";
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
5
 
6
6
  import { ulwLoopCommand } from "../src/cli-commands.js";
7
- import { ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js";
7
+ import { aggregateCodexObjectiveForScope } from "../src/goal-status.js";
8
+ import { CLI_TEST_SCOPE, CLI_TEST_SESSION_ID } from "./fixtures/cli-session.js";
8
9
 
9
10
  let testDir: string;
10
11
  let out: string[];
@@ -15,6 +16,7 @@ beforeEach(async () => {
15
16
  out = [];
16
17
  originalOmoSessionId = process.env["OMO_ULW_LOOP_SESSION_ID"];
17
18
  delete process.env["OMO_ULW_LOOP_SESSION_ID"];
19
+ process.env["OMO_ULW_LOOP_SESSION_ID"] = CLI_TEST_SESSION_ID;
18
20
  vi.spyOn(process, "cwd").mockReturnValue(testDir);
19
21
  vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
20
22
  out.push(chunk.toString());
@@ -39,7 +41,7 @@ function stdoutJson(): Record<string, unknown> {
39
41
  }
40
42
 
41
43
  function codexSnapshot(): string {
42
- return JSON.stringify({ goal: { objective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE, status: "active" } });
44
+ return JSON.stringify({ goal: { objective: aggregateCodexObjectiveForScope(CLI_TEST_SCOPE), status: "active" } });
43
45
  }
44
46
 
45
47
  async function passCriterion(criterionId: string): Promise<void> {
@@ -4,7 +4,8 @@ import { join } from "node:path";
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
5
 
6
6
  import { ulwLoopCommand } from "../src/cli-commands.ts";
7
- import { ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js";
7
+ import { aggregateCodexObjectiveForScope } from "../src/goal-status.js";
8
+ import { CLI_TEST_SCOPE, CLI_TEST_SESSION_ID } from "./fixtures/cli-session.js";
8
9
  import { QA_DIR, qualityGateJson } from "./fixtures/quality-gate-builder.js";
9
10
 
10
11
  let testDir: string;
@@ -24,6 +25,7 @@ beforeEach(async () => {
24
25
  delete process.env["CODEX_SESSION_ID"];
25
26
  delete process.env["CODEX_THREAD_ID"];
26
27
  delete process.env["OMO_ULW_LOOP_SESSION_ID"];
28
+ process.env["OMO_ULW_LOOP_SESSION_ID"] = CLI_TEST_SESSION_ID;
27
29
  vi.spyOn(process, "cwd").mockReturnValue(testDir);
28
30
  vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
29
31
  out.push(chunk.toString());
@@ -56,7 +58,7 @@ function stdoutJson(): Record<string, unknown> {
56
58
  }
57
59
 
58
60
  function codexSnapshot(status: "active" | "complete" = "active"): string {
59
- return JSON.stringify({ goal: { objective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE, status } });
61
+ return JSON.stringify({ goal: { objective: aggregateCodexObjectiveForScope(CLI_TEST_SCOPE), status } });
60
62
  }
61
63
 
62
64
  async function createPlan(brief = "- Goal A\n- Goal B"): Promise<Record<string, unknown>> {
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
5
 
6
6
  import { ulwLoopCommand } from "../src/cli-commands.ts";
7
+ import { CLI_TEST_SESSION_ID } from "./fixtures/cli-session.js";
7
8
 
8
9
  let testDir: string;
9
10
  let out: string[];
@@ -22,6 +23,7 @@ beforeEach(async () => {
22
23
  delete process.env["CODEX_SESSION_ID"];
23
24
  delete process.env["CODEX_THREAD_ID"];
24
25
  delete process.env["OMO_ULW_LOOP_SESSION_ID"];
26
+ process.env["OMO_ULW_LOOP_SESSION_ID"] = CLI_TEST_SESSION_ID;
25
27
  vi.spyOn(process, "cwd").mockReturnValue(testDir);
26
28
  vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
27
29
  out.push(chunk.toString());
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
5
 
6
6
  import { ulwLoopCommand } from "../src/cli-commands.ts";
7
7
  import { ulwLoopLedgerPath } from "../src/paths.ts";
8
+ import { CLI_TEST_SCOPE, CLI_TEST_SESSION_ID } from "./fixtures/cli-session.js";
8
9
 
9
10
  let testDir: string;
10
11
  let out: string[];
@@ -21,6 +22,7 @@ beforeEach(async () => {
21
22
  delete process.env["CODEX_SESSION_ID"];
22
23
  delete process.env["CODEX_THREAD_ID"];
23
24
  delete process.env["OMO_ULW_LOOP_SESSION_ID"];
25
+ process.env["OMO_ULW_LOOP_SESSION_ID"] = CLI_TEST_SESSION_ID;
24
26
  vi.spyOn(process, "cwd").mockReturnValue(testDir);
25
27
  vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
26
28
  out.push(chunk.toString());
@@ -48,7 +50,7 @@ function stdoutJson(): Record<string, unknown> {
48
50
  }
49
51
 
50
52
  async function ledgerKinds(): Promise<string[]> {
51
- const raw = await readFile(ulwLoopLedgerPath(testDir), "utf8");
53
+ const raw = await readFile(ulwLoopLedgerPath(testDir, CLI_TEST_SCOPE), "utf8");
52
54
  return raw
53
55
  .split(/\r?\n/)
54
56
  .filter(Boolean)
@@ -4,7 +4,8 @@ import { join } from "node:path";
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
5
 
6
6
  import { ulwLoopCommand } from "../src/cli-commands.ts";
7
- import { ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js";
7
+ import { aggregateCodexObjectiveForScope } from "../src/goal-status.js";
8
+ import { CLI_TEST_SCOPE, CLI_TEST_SESSION_ID } from "./fixtures/cli-session.js";
8
9
  import { qualityGateJson } from "./fixtures/quality-gate-builder.js";
9
10
 
10
11
  let testDir: string;
@@ -27,6 +28,7 @@ beforeEach(async () => {
27
28
  delete process.env["CODEX_THREAD_ID"];
28
29
  delete process.env["OMO_ULW_LOOP_SESSION_ID"];
29
30
  delete process.env["PI_SESSION_ID"];
31
+ process.env["PI_SESSION_ID"] = CLI_TEST_SESSION_ID;
30
32
  vi.spyOn(process, "cwd").mockReturnValue(testDir);
31
33
  vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
32
34
  out.push(chunk.toString());
@@ -61,7 +63,7 @@ function stdoutJson(): Record<string, unknown> {
61
63
  }
62
64
 
63
65
  function codexSnapshot(status: "active" | "complete" = "active"): string {
64
- return JSON.stringify({ goal: { objective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE, status } });
66
+ return JSON.stringify({ goal: { objective: aggregateCodexObjectiveForScope(CLI_TEST_SCOPE), status } });
65
67
  }
66
68
 
67
69
  async function qualityGate(): Promise<string> {
@@ -101,9 +103,9 @@ describe("ulwLoopCommand create-goals", () => {
101
103
  const parsed = stdoutJson();
102
104
  expect(parsed).toMatchObject({ ok: true });
103
105
  expect(parsed).toHaveProperty("plan.goals.0.successCriteria.0.id", "C001");
104
- expect(await readFile(join(testDir, ".omo/ulw-loop/brief.md"), "utf8")).toContain("Goal A");
105
- expect(await readFile(join(testDir, ".omo/ulw-loop/goals.json"), "utf8")).toContain("successCriteria");
106
- expect(await readFile(join(testDir, ".omo/ulw-loop/ledger.jsonl"), "utf8")).toContain("plan_created");
106
+ expect(await readFile(join(testDir, ".omo/ulw-loop/cli-test/brief.md"), "utf8")).toContain("Goal A");
107
+ expect(await readFile(join(testDir, ".omo/ulw-loop/cli-test/goals.json"), "utf8")).toContain("successCriteria");
108
+ expect(await readFile(join(testDir, ".omo/ulw-loop/cli-test/ledger.jsonl"), "utf8")).toContain("plan_created");
107
109
  });
108
110
 
109
111
  it("#given completed default aggregate #when creating another default plan #then guides to a fresh session", async () => {
@@ -142,7 +144,7 @@ describe("ulwLoopCommand create-goals", () => {
142
144
 
143
145
  expect(await ulwLoopCommand(["status", "--json"])).toBe(0);
144
146
 
145
- expect(stdoutJson()).toMatchObject({ currentAttemptDir: ".omo/evidence/ulw/session/G001-goal-a/a1" });
147
+ expect(stdoutJson()).toMatchObject({ currentAttemptDir: ".omo/evidence/ulw/cli-test/G001-goal-a/a1" });
146
148
  });
147
149
 
148
150
  it("#given two session ids #when creating goals #then writes isolated session-scoped plans", async () => {
@@ -212,7 +214,7 @@ describe("ulwLoopCommand create-goals", () => {
212
214
 
213
215
  expect(code).toBe(1);
214
216
  expect(err.join("")).toContain("--session-id requires a non-empty value");
215
- await expect(readFile(join(testDir, ".omo/ulw-loop/goals.json"), "utf8")).rejects.toThrow();
217
+ await expect(readFile(join(testDir, ".omo/ulw-loop/cli-test/goals.json"), "utf8")).rejects.toThrow();
216
218
  });
217
219
 
218
220
  it("#given an empty --session-id= #when creating goals #then it fails and writes no plan", async () => {
@@ -220,6 +222,6 @@ describe("ulwLoopCommand create-goals", () => {
220
222
 
221
223
  expect(code).toBe(1);
222
224
  expect(err.join("")).toContain("--session-id requires a non-empty value");
223
- await expect(readFile(join(testDir, ".omo/ulw-loop/goals.json"), "utf8")).rejects.toThrow();
225
+ await expect(readFile(join(testDir, ".omo/ulw-loop/cli-test/goals.json"), "utf8")).rejects.toThrow();
224
226
  });
225
227
  });
@@ -74,7 +74,7 @@ afterEach(async () => {
74
74
 
75
75
  describe("dist/cli.js entrypoint dispatch", () => {
76
76
  it("#given no plan #when invoked with bare 'status --json' #then routes into ulw-loop instead of unknown command", async () => {
77
- const result = await runCli(["status", "--json"]);
77
+ const result = await runCli(["status", "--session-id", "s1", "--json"]);
78
78
 
79
79
  const combined = `${result.stdout}${result.stderr}`;
80
80
  expect(combined).toContain("No ulw-loop plan found");
@@ -83,7 +83,7 @@ describe("dist/cli.js entrypoint dispatch", () => {
83
83
  });
84
84
 
85
85
  it("#given no plan #when invoked with legacy 'ulw-loop status --json' #then still routes into ulw-loop", async () => {
86
- const result = await runCli(["ulw-loop", "status", "--json"]);
86
+ const result = await runCli(["ulw-loop", "status", "--session-id", "s1", "--json"]);
87
87
 
88
88
  const combined = `${result.stdout}${result.stderr}`;
89
89
  expect(combined).toContain("No ulw-loop plan found");
@@ -91,6 +91,16 @@ describe("dist/cli.js entrypoint dispatch", () => {
91
91
  expect(result.code).toBe(1);
92
92
  });
93
93
 
94
+ it("#given no session flag and no session env #when invoked with 'status --json' #then refuses the unscoped root instead of reading it", async () => {
95
+ const result = await runCli(["status", "--json"]);
96
+
97
+ expect(result.code).toBe(1);
98
+ expect(JSON.parse(result.stdout)).toMatchObject({
99
+ ok: false,
100
+ error: { code: "ULW_LOOP_SESSION_SCOPE_REQUIRED", details: { flag: "--session-id" } },
101
+ });
102
+ });
103
+
94
104
  it("#given the top-level entrypoint #when invoked with 'help' #then prints the merged hook and subcommand usage and exits 0", async () => {
95
105
  const result = await runCli(["help"]);
96
106
 
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
5
 
6
6
  import { ulwLoopCommand } from "../src/cli-commands.ts";
7
+ import { CLI_TEST_SESSION_ID } from "./fixtures/cli-session.js";
7
8
 
8
9
  let testDir: string;
9
10
  let out: string[];
@@ -22,6 +23,7 @@ beforeEach(async () => {
22
23
  delete process.env["CODEX_SESSION_ID"];
23
24
  delete process.env["CODEX_THREAD_ID"];
24
25
  delete process.env["OMO_ULW_LOOP_SESSION_ID"];
26
+ process.env["OMO_ULW_LOOP_SESSION_ID"] = CLI_TEST_SESSION_ID;
25
27
  vi.spyOn(process, "cwd").mockReturnValue(testDir);
26
28
  vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
27
29
  out.push(chunk.toString());
@@ -0,0 +1,144 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import { ulwLoopCommand } from "../src/cli-commands.ts";
8
+ import { ulwLoopAttemptEvidenceDir } from "../src/paths.ts";
9
+
10
+ const SESSION_ENV_KEYS = ["OMO_ULW_LOOP_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID", "PI_SESSION_ID"] as const;
11
+ const NOW = "2026-05-23T00:00:00.000Z";
12
+
13
+ let testDir: string;
14
+ let out: string[];
15
+ let err: string[];
16
+ let savedEnv: Record<string, string | undefined>;
17
+
18
+ beforeEach(async () => {
19
+ testDir = await mkdtemp(join(tmpdir(), "ug-cli-scope-"));
20
+ out = [];
21
+ err = [];
22
+ savedEnv = {};
23
+ for (const key of SESSION_ENV_KEYS) {
24
+ savedEnv[key] = process.env[key];
25
+ delete process.env[key];
26
+ }
27
+ vi.spyOn(process, "cwd").mockReturnValue(testDir);
28
+ vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
29
+ out.push(chunk.toString());
30
+ return true;
31
+ });
32
+ vi.spyOn(process.stderr, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
33
+ err.push(chunk.toString());
34
+ return true;
35
+ });
36
+ });
37
+
38
+ afterEach(async () => {
39
+ vi.restoreAllMocks();
40
+ for (const key of SESSION_ENV_KEYS) {
41
+ const saved = savedEnv[key];
42
+ if (saved === undefined) delete process.env[key];
43
+ else process.env[key] = saved;
44
+ }
45
+ await rm(testDir, { recursive: true, force: true });
46
+ });
47
+
48
+ function stdoutJson(): Record<string, unknown> {
49
+ return JSON.parse(out.join(""));
50
+ }
51
+
52
+ async function writeLegacyRootPlan(): Promise<void> {
53
+ await mkdir(join(testDir, ".omo", "ulw-loop", "other-session"), { recursive: true });
54
+ await writeFile(
55
+ join(testDir, ".omo", "ulw-loop", "goals.json"),
56
+ JSON.stringify({
57
+ version: 1,
58
+ createdAt: NOW,
59
+ updatedAt: NOW,
60
+ briefPath: ".omo/ulw-loop/brief.md",
61
+ goalsPath: ".omo/ulw-loop/goals.json",
62
+ ledgerPath: ".omo/ulw-loop/ledger.jsonl",
63
+ goals: [],
64
+ }),
65
+ );
66
+ }
67
+
68
+ describe("ulwLoopCommand session scope", () => {
69
+ describe("#given no --session-id and no session env", () => {
70
+ it("#when create-goals runs #then it refuses with ULW_LOOP_SESSION_SCOPE_REQUIRED and writes nothing", async () => {
71
+ const code = await ulwLoopCommand(["create-goals", "--brief", "- Goal A", "--json"]);
72
+
73
+ expect(code).toBe(1);
74
+ expect(err.join("")).toBe("");
75
+ expect(stdoutJson()).toMatchObject({
76
+ ok: false,
77
+ error: {
78
+ code: "ULW_LOOP_SESSION_SCOPE_REQUIRED",
79
+ details: { flag: "--session-id", existingSessionIds: [] },
80
+ },
81
+ });
82
+ expect(existsSync(join(testDir, ".omo"))).toBe(false);
83
+ });
84
+
85
+ it("#when a legacy root plan exists #then status does not read it and lists the real session ids", async () => {
86
+ await writeLegacyRootPlan();
87
+
88
+ const code = await ulwLoopCommand(["status", "--json"]);
89
+
90
+ expect(code).toBe(1);
91
+ expect(stdoutJson()).toMatchObject({
92
+ ok: false,
93
+ error: { code: "ULW_LOOP_SESSION_SCOPE_REQUIRED", details: { existingSessionIds: ["other-session"] } },
94
+ });
95
+ expect(out.join("")).not.toContain('"goals"');
96
+ });
97
+
98
+ it("#when the human-readable form is used #then the error explains the flag and the env keys", async () => {
99
+ const code = await ulwLoopCommand(["status"]);
100
+
101
+ expect(code).toBe(1);
102
+ expect(err.join("")).toContain("--session-id <id>");
103
+ expect(err.join("")).toContain("PI_SESSION_ID");
104
+ });
105
+
106
+ it("#when help is requested #then it prints usage without needing a scope", async () => {
107
+ expect(await ulwLoopCommand(["help"])).toBe(0);
108
+ expect(await ulwLoopCommand(["status", "--help"])).toBe(0);
109
+ expect(err.join("")).toBe("");
110
+ });
111
+ });
112
+
113
+ describe("#given a session env key", () => {
114
+ it("#when status runs #then the scope resolves to that session directory", async () => {
115
+ process.env["PI_SESSION_ID"] = "env-session";
116
+
117
+ const code = await ulwLoopCommand(["status", "--json"]);
118
+
119
+ expect(code).toBe(1);
120
+ expect(stdoutJson()).toMatchObject({
121
+ ok: false,
122
+ error: {
123
+ code: "ULW_LOOP_PLAN_MISSING",
124
+ message: expect.stringContaining(".omo/ulw-loop/env-session/goals.json"),
125
+ },
126
+ });
127
+ });
128
+ });
129
+ });
130
+
131
+ describe("ulwLoopAttemptEvidenceDir", () => {
132
+ it("#given no session scope #when resolving an attempt dir #then it refuses instead of inventing a placeholder", () => {
133
+ expect(() => ulwLoopAttemptEvidenceDir("G001", 1)).toThrow(
134
+ expect.objectContaining({ code: "ULW_LOOP_SESSION_SCOPE_REQUIRED" }),
135
+ );
136
+ expect(() => ulwLoopAttemptEvidenceDir("G001", 1, { sessionId: " " })).toThrow(
137
+ expect.objectContaining({ code: "ULW_LOOP_SESSION_SCOPE_REQUIRED" }),
138
+ );
139
+ });
140
+
141
+ it("#given a session scope #when resolving an attempt dir #then it lives under that session", () => {
142
+ expect(ulwLoopAttemptEvidenceDir("G001", 2, { sessionId: "s1" })).toBe(".omo/evidence/ulw/s1/G001/a2");
143
+ });
144
+ });
@@ -7,6 +7,7 @@ import { ulwLoopCommand } from "../src/cli-commands.js";
7
7
  import { ulwLoopDir } from "../src/paths.js";
8
8
  import { writePlan } from "../src/plan-io.js";
9
9
  import type { UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/types.js";
10
+ import { CLI_TEST_SCOPE, CLI_TEST_SESSION_ID } from "./fixtures/cli-session.js";
10
11
 
11
12
  const NOW = "2026-05-23T00:00:00.000Z";
12
13
  const SESSION_ENV_KEYS = ["OMO_ULW_LOOP_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID", "PI_SESSION_ID"] as const;
@@ -25,6 +26,7 @@ beforeEach(async () => {
25
26
  savedEnv[key] = process.env[key];
26
27
  delete process.env[key];
27
28
  }
29
+ process.env["OMO_ULW_LOOP_SESSION_ID"] = CLI_TEST_SESSION_ID;
28
30
  vi.spyOn(process, "cwd").mockReturnValue(testDir);
29
31
  vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
30
32
  out.push(chunk.toString());
@@ -87,9 +89,9 @@ function plan(goals: UlwLoopItem[], overrides: Partial<UlwLoopPlan> = {}): UlwLo
87
89
  evidenceLayoutVersion: 2,
88
90
  createdAt: NOW,
89
91
  updatedAt: NOW,
90
- briefPath: ".omo/ulw-loop/brief.md",
91
- goalsPath: ".omo/ulw-loop/goals.json",
92
- ledgerPath: ".omo/ulw-loop/ledger.jsonl",
92
+ briefPath: ".omo/ulw-loop/cli-test/brief.md",
93
+ goalsPath: ".omo/ulw-loop/cli-test/goals.json",
94
+ ledgerPath: ".omo/ulw-loop/cli-test/ledger.jsonl",
93
95
  goals,
94
96
  };
95
97
  Object.assign(result, overrides);
@@ -103,8 +105,8 @@ function legacyLayoutPlan(goals: UlwLoopItem[], overrides: Partial<UlwLoopPlan>
103
105
  }
104
106
 
105
107
  async function seed(seedPlan: UlwLoopPlan): Promise<void> {
106
- await mkdir(ulwLoopDir(testDir), { recursive: true });
107
- await writePlan(testDir, seedPlan);
108
+ await mkdir(ulwLoopDir(testDir, CLI_TEST_SCOPE), { recursive: true });
109
+ await writePlan(testDir, seedPlan, CLI_TEST_SCOPE);
108
110
  }
109
111
 
110
112
  describe("#given a plan with no goals", () => {
@@ -9,6 +9,7 @@ import { parseSteeringProposals } from "../src/cli-steering.js";
9
9
  import { ulwLoopGoalsPath } from "../src/paths.js";
10
10
  import { writePlan } from "../src/plan-io.js";
11
11
  import type { UlwLoopPlan } from "../src/types.js";
12
+ import { CLI_TEST_SCOPE } from "./fixtures/cli-session.js";
12
13
 
13
14
  const NOW = "2026-05-23T00:00:00.000Z";
14
15
 
@@ -45,9 +46,11 @@ describe("parseSteeringProposals", () => {
45
46
  it("#given kind and proposals-json #when steering through CLI #then rejects conflict without mutation", async () => {
46
47
  const repo = await mkdtemp(join(tmpdir(), "ug-cli-steer-batch-conflict-"));
47
48
  const out: string[] = [];
49
+ const originalOmoSessionId = process.env["OMO_ULW_LOOP_SESSION_ID"];
48
50
  try {
49
- await writePlan(repo, plan());
50
- const before = await readFile(ulwLoopGoalsPath(repo), "utf8");
51
+ process.env["OMO_ULW_LOOP_SESSION_ID"] = CLI_TEST_SCOPE.sessionId;
52
+ await writePlan(repo, plan(), CLI_TEST_SCOPE);
53
+ const before = await readFile(ulwLoopGoalsPath(repo, CLI_TEST_SCOPE), "utf8");
51
54
  vi.spyOn(process, "cwd").mockReturnValue(repo);
52
55
  vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
53
56
  out.push(chunk.toString());
@@ -58,8 +61,10 @@ describe("parseSteeringProposals", () => {
58
61
  expect(await ulwLoopCommand(["steer", "--kind", "annotate_ledger", "--evidence", "flag", "--rationale", "flag", "--proposals-json", '[{"kind":"annotate_ledger","evidence":"batch","rationale":"batch"}]', "--json"])).toBe(1);
59
62
 
60
63
  expect(JSON.parse(out.join(""))).toHaveProperty("error.code", "ULW_LOOP_STEERING_BATCH_CONFLICT");
61
- expect(await readFile(ulwLoopGoalsPath(repo), "utf8")).toBe(before);
64
+ expect(await readFile(ulwLoopGoalsPath(repo, CLI_TEST_SCOPE), "utf8")).toBe(before);
62
65
  } finally {
66
+ if (originalOmoSessionId === undefined) delete process.env["OMO_ULW_LOOP_SESSION_ID"];
67
+ else process.env["OMO_ULW_LOOP_SESSION_ID"] = originalOmoSessionId;
63
68
  vi.restoreAllMocks();
64
69
  await rm(repo, { recursive: true, force: true });
65
70
  }
@@ -4,13 +4,17 @@ import { join } from "node:path";
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
5
 
6
6
  import { ulwLoopCommand } from "../src/cli-commands.js";
7
+ import { CLI_TEST_SESSION_ID } from "./fixtures/cli-session.js";
7
8
 
8
9
  let testDir: string;
9
10
  let out: string[];
11
+ let originalOmoSessionId: string | undefined;
10
12
 
11
13
  beforeEach(async () => {
12
14
  testDir = await mkdtemp(join(tmpdir(), "ug-cli-validation-batch-"));
13
15
  out = [];
16
+ originalOmoSessionId = process.env["OMO_ULW_LOOP_SESSION_ID"];
17
+ process.env["OMO_ULW_LOOP_SESSION_ID"] = CLI_TEST_SESSION_ID;
14
18
  vi.spyOn(process, "cwd").mockReturnValue(testDir);
15
19
  vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array): boolean => {
16
20
  out.push(chunk.toString());
@@ -21,6 +25,8 @@ beforeEach(async () => {
21
25
 
22
26
  afterEach(async () => {
23
27
  vi.restoreAllMocks();
28
+ if (originalOmoSessionId === undefined) delete process.env["OMO_ULW_LOOP_SESSION_ID"];
29
+ else process.env["OMO_ULW_LOOP_SESSION_ID"] = originalOmoSessionId;
24
30
  await rm(testDir, { recursive: true, force: true });
25
31
  });
26
32
 
@@ -0,0 +1,4 @@
1
+ // Every CLI-level test runs inside one explicit session scope: the CLI refuses to
2
+ // touch the unscoped .omo/ulw-loop root, so state and evidence live under this id.
3
+ export const CLI_TEST_SESSION_ID = "cli-test";
4
+ export const CLI_TEST_SCOPE = { sessionId: CLI_TEST_SESSION_ID } as const;
@@ -2,9 +2,10 @@ import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
4
  import { reviewerRolesFor, type UlwLoopToolkitSurface } from "../../src/surface.js";
5
+ import { CLI_TEST_SESSION_ID } from "./cli-session.js";
5
6
 
6
7
  // v2 evidence layout: artifacts must live inside the goal's current attempt dir
7
- export function qaDirFor(goalId: string, attempt = 1, sessionId = "session"): string {
8
+ export function qaDirFor(goalId: string, attempt = 1, sessionId = CLI_TEST_SESSION_ID): string {
8
9
  return `.omo/evidence/ulw/${sessionId}/${goalId}/a${attempt}`;
9
10
  }
10
11