lazycodex-ai 5.0.0-beta.53 → 5.0.0-beta.55

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 (80) hide show
  1. package/dist/cli/index.js +53 -19
  2. package/dist/cli-node/index.js +53 -19
  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 +1 -1
  25. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  26. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.d.ts +5 -1
  27. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.js +14 -34
  28. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-continuation.js +1 -4
  29. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.d.ts +0 -18
  30. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.js +1 -88
  31. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.d.ts +2 -0
  32. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +10 -3
  33. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-arg-parser.js +2 -2
  34. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +2 -0
  35. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +86 -174
  36. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +2 -18
  37. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +23 -33
  38. package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.d.ts +2 -0
  39. package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +16 -9
  40. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  41. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  42. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-codex-validation.ts +18 -59
  43. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-continuation.ts +1 -6
  44. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-reconciliation.ts +1 -124
  45. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +11 -3
  46. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-arg-parser.ts +10 -4
  47. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +2 -0
  48. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +31 -50
  49. package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +29 -10
  50. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +0 -82
  51. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint.test.ts +16 -22
  52. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-checkpoint.test.ts +27 -0
  53. package/packages/omo-codex/plugin/components/ulw-loop/test/cli-json-errors.test.ts +25 -0
  54. package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-snapshot.test.ts +34 -11
  55. package/packages/omo-codex/plugin/components/ulw-loop/test/guided-recovery.test.ts +9 -47
  56. package/packages/omo-codex/plugin/components/ulw-loop/test/review-blockers.test.ts +9 -7
  57. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  58. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  59. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  60. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  61. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  62. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  63. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  64. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  65. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  66. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  67. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  68. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  69. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  70. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  71. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  72. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  73. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  74. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  75. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  76. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  77. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  78. package/packages/omo-codex/plugin/package-lock.json +12 -12
  79. package/packages/omo-codex/plugin/package.json +1 -1
  80. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
@@ -2,7 +2,15 @@ import { existsSync } from "node:fs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { resolve } from "node:path";
4
4
 
5
- export type CodexGoalSnapshotStatus = "active" | "complete" | "cancelled" | "failed" | "unknown";
5
+ export type CodexGoalSnapshotStatus =
6
+ | "active"
7
+ | "complete"
8
+ | "paused"
9
+ | "usage_limited"
10
+ | "budget_limited"
11
+ | "cancelled"
12
+ | "failed"
13
+ | "unknown";
6
14
 
7
15
  export interface CodexGoalSnapshot {
8
16
  available: boolean;
@@ -20,10 +28,7 @@ export interface CodexGoalReconciliation {
20
28
 
21
29
  export interface ReconcileCodexGoalOptions {
22
30
  expectedObjective: string;
23
- acceptedObjectives?: readonly string[];
24
- allowedStatuses?: readonly CodexGoalSnapshotStatus[];
25
- requireSnapshot?: boolean;
26
- requireComplete?: boolean;
31
+ readonly acceptedObjectives?: readonly string[];
27
32
  }
28
33
 
29
34
  export class CodexGoalSnapshotError extends Error {}
@@ -32,14 +37,21 @@ function safeObject(value: unknown): Record<string, unknown> {
32
37
  }
33
38
 
34
39
  function safeString(value: unknown): string {
40
+ return typeof value === "string" ? value : "";
41
+ }
42
+
43
+ function safeStatusString(value: unknown): string {
35
44
  return typeof value === "string" ? value.trim() : "";
36
45
  }
37
46
 
38
47
  function normalizeStatus(value: unknown): CodexGoalSnapshotStatus {
39
- const status = safeString(value).toLowerCase();
48
+ const status = safeStatusString(value).toLowerCase();
40
49
  if (status === "complete" || status === "completed" || status === "done") return "complete";
41
50
  if (status === "cancelled" || status === "canceled") return "cancelled";
42
51
  if (status === "failed" || status === "failure") return "failed";
52
+ if (status === "paused") return "paused";
53
+ if (status === "usage_limited") return "usage_limited";
54
+ if (status === "budget_limited") return "budget_limited";
43
55
  if (status === "active" || status === "in_progress" || status === "pending" || status === "running") return "active";
44
56
  return "unknown";
45
57
  }
@@ -100,38 +112,30 @@ export function reconcileCodexGoalSnapshot(
100
112
  const errors: string[] = [];
101
113
  const warnings: string[] = [];
102
114
 
115
+ const expected = options.expectedObjective;
116
+ const normalizedExpected = normalizeObjective(expected);
103
117
  if (!effectiveSnapshot.available) {
104
- const message =
105
- "Codex goal snapshot is absent or reports no active goal; call get_goal and pass its JSON with --codex-goal-json.";
106
- if (options.requireSnapshot) errors.push(message);
107
- else warnings.push(message);
118
+ warnings.push(`call get_goal; if none, create_goal with codexObjective "${expected}" verbatim`);
108
119
  return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
109
120
  }
110
121
 
111
- const expected = normalizeObjective(options.expectedObjective);
112
122
  const accepted = new Set(
113
- [expected, ...(options.acceptedObjectives ?? []).map((objective) => normalizeObjective(objective))].filter(
114
- Boolean,
115
- ),
123
+ [
124
+ normalizedExpected,
125
+ ...(options.acceptedObjectives ?? []).map((objective) => normalizeObjective(objective)),
126
+ ].filter(Boolean),
116
127
  );
117
128
  const actual = normalizeObjective(effectiveSnapshot.objective ?? "");
118
- if (!actual) {
119
- errors.push("Codex goal snapshot is missing objective text.");
120
- } else if (!accepted.has(actual)) {
121
- errors.push(`Codex goal objective mismatch: expected "${expected}", got "${actual}".`);
129
+ if (actual && !accepted.has(normalizeObjective(actual))) {
130
+ warnings.push(`driver_objective_differs: expected "${expected}", got "${actual}".`);
122
131
  }
123
132
 
124
- const allowed = options.allowedStatuses ?? (options.requireComplete ? ["complete"] : ["active", "complete"]);
125
133
  const actualStatus = effectiveSnapshot.status ?? "unknown";
126
- if (!allowed.includes(actualStatus)) {
127
- errors.push(`Codex goal status mismatch: expected ${allowed.join(" or ")}, got ${actualStatus}.`);
128
- }
129
- if (options.requireComplete && actualStatus !== "complete") {
130
- errors.push(
131
- 'Codex goal is not complete; call update_goal({status: "complete"}) only after the objective is actually complete, then pass the fresh get_goal JSON.',
132
- );
134
+ if (actualStatus === "paused" || actualStatus === "usage_limited" || actualStatus === "budget_limited") {
135
+ warnings.push("/goal resume or raise the budget");
133
136
  }
134
-
137
+ if (actualStatus === "complete")
138
+ warnings.push(`driver closed early: call create_goal with codexObjective "${expected}" verbatim`);
135
139
  return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
136
140
  }
137
141
 
@@ -139,26 +143,3 @@ export function formatCodexGoalReconciliation(reconciliation: CodexGoalReconcili
139
143
  const parts = [...reconciliation.errors, ...reconciliation.warnings];
140
144
  return parts.join(" ");
141
145
  }
142
-
143
- export interface CodexGoalMismatchRecovery {
144
- readonly message: string;
145
- readonly details: { readonly expectedObjective: string; readonly receivedObjective: string };
146
- }
147
-
148
- /**
149
- * The reconciliation errors normalize whitespace for comparison, which makes the
150
- * quoted objective unusable as a copy source. Recovery therefore carries the
151
- * plan's `codexObjective` verbatim so the agent can paste it into `update_goal`.
152
- */
153
- export function codexGoalMismatchRecovery(
154
- expectedObjective: string,
155
- snapshot: CodexGoalSnapshot | null | undefined,
156
- ): CodexGoalMismatchRecovery {
157
- const receivedObjective = snapshot?.objective ?? "";
158
- const message = [
159
- "Recovery: the Codex goal objective must equal the plan's codexObjective exactly — copy the expected value below into update_goal and re-run get_goal.",
160
- `expected codexObjective: ${expectedObjective}`,
161
- `received objective: ${receivedObjective || "(none)"}`,
162
- ].join("\n");
163
- return { message, details: { expectedObjective, receivedObjective } };
164
- }
@@ -1,8 +1,12 @@
1
1
  // biome-ignore-all format: compact port must stay within the requested pure LOC budget.
2
2
 
3
- import { codexSnapshotMismatchError } from "./checkpoint-reconciliation.js";
4
- import { readCodexGoalSnapshotInput, reconcileCodexGoalSnapshot } from "./codex-goal-snapshot.js";
5
- import { codexGoalMode, compatibleCodexObjectives, expectedCodexObjective, isFinalRunCompletionCandidate } from "./goal-status.js";
3
+ import {
4
+ CodexGoalSnapshotError,
5
+ formatCodexGoalReconciliation,
6
+ readCodexGoalSnapshotInput,
7
+ reconcileCodexGoalSnapshot,
8
+ } from "./codex-goal-snapshot.js";
9
+ import { compatibleCodexObjectives, expectedCodexObjective, isFinalRunCompletionCandidate } from "./goal-status.js";
6
10
  import type { UlwLoopScope } from "./paths.js";
7
11
  import { seedDefaultSuccessCriteria } from "./plan-crud.js";
8
12
  import { appendLedger, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
@@ -10,7 +14,14 @@ import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan } from "./types.js";
10
14
  import { iso, UlwLoopError } from "./types.js";
11
15
 
12
16
  export interface RecordFinalReviewBlockersArgs { readonly goalId: string; readonly title: string; readonly objective: string; readonly evidence: string; readonly codexGoalJson: string }
13
- export interface RecordFinalReviewBlockersResult { readonly plan: UlwLoopPlan; readonly blockedGoal: UlwLoopItem; readonly newGoal: UlwLoopItem; readonly ledgerEntries: UlwLoopLedgerEntry[] }
17
+ export interface RecordFinalReviewBlockersResult {
18
+ readonly plan: UlwLoopPlan;
19
+ readonly blockedGoal: UlwLoopItem;
20
+ readonly newGoal: UlwLoopItem;
21
+ readonly ledgerEntries: UlwLoopLedgerEntry[];
22
+ readonly nextActions: readonly string[];
23
+ readonly warnings: readonly string[];
24
+ }
14
25
 
15
26
  const BLOCKER_FIELDS = "blockedReason blockerSignature blockerOccurrenceCount requiredExternalDecision nonRetriable failedAt failureReason completedAt blocker blockerEvidence blockerOccurrences blockedAt".split(" ");
16
27
 
@@ -55,10 +66,11 @@ export async function recordFinalReviewBlockers(
55
66
  if (!isFinalRunCompletionCandidate(plan, goal)) ulwLoopError(`${goal.id} is not final.`, "ulw_loop_not_final_story");
56
67
 
57
68
  const snapshot = await readCodexGoalSnapshotInput(args.codexGoalJson, repoRoot);
58
- const aggregate = codexGoalMode(plan) === "aggregate";
59
- const expected = expectedCodexObjective(plan, goal);
60
- const reconciliation = reconcileCodexGoalSnapshot(snapshot, { expectedObjective: expected, ...(aggregate ? { acceptedObjectives: compatibleCodexObjectives(plan) } : {}), allowedStatuses: ["active"], requireSnapshot: true, requireComplete: false });
61
- if (!reconciliation.ok) throw codexSnapshotMismatchError({ reconciliation, snapshot, expectedObjective: expected });
69
+ const reconciliation = reconcileCodexGoalSnapshot(snapshot, {
70
+ expectedObjective: expectedCodexObjective(plan, goal),
71
+ acceptedObjectives: compatibleCodexObjectives(plan),
72
+ });
73
+ if (!reconciliation.ok) throw new CodexGoalSnapshotError(formatCodexGoalReconciliation(reconciliation));
62
74
 
63
75
  const now = iso();
64
76
  for (const field of BLOCKER_FIELDS) Reflect.deleteProperty(goal, field);
@@ -70,7 +82,7 @@ export async function recordFinalReviewBlockers(
70
82
  const newGoal = appendBlockerGoal(plan, args, now);
71
83
  plan.updatedAt = now;
72
84
 
73
- const codexGoal = reconciliation.snapshot.raw;
85
+ const codexGoal = snapshot?.raw;
74
86
  const blockedEntry: UlwLoopLedgerEntry = { at: now, kind: "goal_review_blocked", goalId: goal.id, status: goal.status, evidence: args.evidence, codexGoal };
75
87
  const addedEntry: UlwLoopLedgerEntry = { at: now, kind: "goal_added", goalId: newGoal.id, status: newGoal.status, evidence: args.evidence, message: newGoal.title };
76
88
  const summaryEntry: UlwLoopLedgerEntry = { at: now, kind: "goal_review_blocked", goalId: goal.id, status: goal.status, evidence: args.evidence, codexGoal, message: `Review blockers recorded; appended ${newGoal.id}.` };
@@ -78,6 +90,13 @@ export async function recordFinalReviewBlockers(
78
90
  const ledgerEntries = [blockedEntry, addedEntry, summaryEntry];
79
91
  await writePlan(repoRoot, plan, scope);
80
92
  for (const entry of ledgerEntries) await appendLedger(repoRoot, entry, scope);
81
- return { plan, blockedGoal: goal, newGoal, ledgerEntries };
93
+ return {
94
+ plan,
95
+ blockedGoal: goal,
96
+ newGoal,
97
+ ledgerEntries,
98
+ nextActions: reconciliation.warnings,
99
+ warnings: reconciliation.warnings.filter((warning) => warning.startsWith("driver_objective_differs")),
100
+ };
82
101
  });
83
102
  }
@@ -1,9 +1,6 @@
1
- import { writeFile } from "node:fs/promises";
2
1
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
2
 
4
3
  import { checkpointUlwLoop } from "../src/checkpoint.js";
5
- import { ulwLoopBriefPath } from "../src/paths.js";
6
- import { startNextUlwLoop } from "../src/plan-crud.js";
7
4
  import { criterion, expectCode, goal, passGoal, plan, repoWith, snapshot } from "./fixtures/checkpoint-builders.js";
8
5
  import { MISSING_ARTIFACT_PATH, qualityGateJson } from "./fixtures/quality-gate-builder.js";
9
6
 
@@ -134,7 +131,6 @@ describe("checkpointUlwLoop final story", () => {
134
131
  } catch (error) {
135
132
  expect(error).toBeInstanceOf(Error);
136
133
  if (!(error instanceof Error)) throw error;
137
- expect(error.message).toContain("Codex goal objective mismatch");
138
134
  expect(error.message).toContain("manualQa.surfaceEvidence");
139
135
  expect(error.message).toContain("gateReview.recommendation");
140
136
  }
@@ -216,84 +212,6 @@ describe("checkpointUlwLoop final story", () => {
216
212
  );
217
213
  });
218
214
 
219
- it("ACCEPTS complete when task-scoped completed Codex objective maps to the ulw-loop brief", async () => {
220
- const taskObjective = "Fix ulw-loop objective mismatch and install local ulw";
221
- const repo = await repoWith(plan([passGoal("G001")], { activeGoalId: "G001" }));
222
- await writeFile(ulwLoopBriefPath(repo), `${taskObjective}\n`, "utf8");
223
-
224
- const result = await checkpointUlwLoop(repo, {
225
- goalId: "G001",
226
- status: "complete",
227
- evidence: "final implementation complete and quality gate passed",
228
- codexGoalJson: snapshot("complete", taskObjective),
229
- qualityGateJson: await qualityGateJson(repo),
230
- });
231
-
232
- expect(result.aggregateCompletion?.status).toBe("complete");
233
- expect(result.ledgerEntry.kind).toBe("aggregate_completed");
234
- });
235
-
236
- it("ACCEPTS complete when active task-scoped Codex objective maps to the ulw-loop brief", async () => {
237
- const taskObjective = "Create only research artifacts with source evidence";
238
- const repo = await repoWith(plan([passGoal("G001")], { activeGoalId: "G001" }));
239
- await writeFile(ulwLoopBriefPath(repo), `${taskObjective}\n`, "utf8");
240
-
241
- const result = await checkpointUlwLoop(repo, {
242
- goalId: "G001",
243
- status: "complete",
244
- evidence: "final implementation complete and quality gate passed",
245
- codexGoalJson: snapshot("active", taskObjective),
246
- qualityGateJson: await qualityGateJson(repo),
247
- });
248
-
249
- expect(result.aggregateCompletion?.status).toBe("complete");
250
- expect(result.ledgerEntry.kind).toBe("aggregate_completed");
251
- });
252
-
253
- it("explains final task-scoped objective mapping when completed Codex objective is unrelated", async () => {
254
- const repo = await repoWith(plan([passGoal("G001")], { activeGoalId: "G001" }));
255
- await writeFile(ulwLoopBriefPath(repo), "Fix ulw-loop objective mismatch and install local ulw\n", "utf8");
256
-
257
- await expect(
258
- checkpointUlwLoop(repo, {
259
- goalId: "G001",
260
- status: "complete",
261
- evidence: "final implementation complete and quality gate passed",
262
- codexGoalJson: snapshot("complete", "unrelated completed task"),
263
- qualityGateJson: await qualityGateJson(repo),
264
- }),
265
- ).rejects.toThrow("Final task-scoped aggregate reconciliation");
266
- });
267
-
268
- it("keeps aggregate open when non-final task-scoped Codex completion maps to the brief", async () => {
269
- const taskObjective = "Implement first accepted story";
270
- const first = goal({
271
- id: "G001",
272
- status: "in_progress",
273
- successCriteria: [
274
- criterion("C001", "pass", { essential: true }),
275
- criterion("C002", "pending", { essential: false }),
276
- ],
277
- });
278
- const second = goal({ id: "G002", status: "pending", objective: "Implement second accepted story" });
279
- const repo = await repoWith(plan([first, second], { activeGoalId: "G001" }));
280
- await writeFile(ulwLoopBriefPath(repo), `${taskObjective}\n`, "utf8");
281
-
282
- const result = await checkpointUlwLoop(repo, {
283
- goalId: "G001",
284
- status: "complete",
285
- evidence: "G001 updated .omo/ulw-loop/goals.json after implementation completed and validation passed",
286
- codexGoalJson: snapshot("complete", taskObjective),
287
- qualityGateJson: await qualityGateJson(repo),
288
- });
289
- const next = await startNextUlwLoop(repo);
290
-
291
- expect(result.aggregateCompletion).toBeUndefined();
292
- expect(result.plan.aggregateCompletion).toBeUndefined();
293
- expect(result.ledgerEntry.kind).toBe("goal_completed");
294
- expect(next).toMatchObject({ goal: { id: "G002", status: "in_progress" } });
295
- });
296
-
297
215
  it("requires all criteria for per-story completion", async () => {
298
216
  const current = goal({
299
217
  successCriteria: [
@@ -127,33 +127,27 @@ describe("checkpointUlwLoop reconciliation (status=complete)", () => {
127
127
  ).resolves.toMatchObject({ goal: { status: "complete" } });
128
128
  });
129
129
 
130
- it("throws on mismatched objective", async () => {
130
+ it("warns when objective mismatches", async () => {
131
131
  const repo = await repoWith(plan([passGoal("G001"), goal({ id: "G002", status: "pending" })]));
132
132
 
133
- await expectCode(
134
- () =>
135
- checkpointUlwLoop(repo, {
136
- goalId: "G001",
137
- status: "complete",
138
- evidence: "work complete and validation passed",
139
- codexGoalJson: snapshot("active", "wrong objective"),
140
- }),
141
- "ulw_loop_codex_snapshot_mismatch",
142
- );
133
+ const result = await checkpointUlwLoop(repo, {
134
+ goalId: "G001",
135
+ status: "complete",
136
+ evidence: "work complete and validation passed",
137
+ codexGoalJson: snapshot("active", "wrong objective"),
138
+ });
139
+ expect(result.warnings.join(" ")).toContain("driver_objective_differs");
143
140
  });
144
141
 
145
- it("throws on mismatched status (snapshot complete when expected active)", async () => {
142
+ it("advises create_goal when the driver closed early", async () => {
146
143
  const repo = await repoWith(plan([passGoal("G001"), goal({ id: "G002", status: "pending" })]));
147
144
 
148
- await expectCode(
149
- () =>
150
- checkpointUlwLoop(repo, {
151
- goalId: "G001",
152
- status: "complete",
153
- evidence: "work complete and validation passed",
154
- codexGoalJson: snapshot("complete"),
155
- }),
156
- "ulw_loop_codex_snapshot_mismatch",
157
- );
145
+ const result = await checkpointUlwLoop(repo, {
146
+ goalId: "G001",
147
+ status: "complete",
148
+ evidence: "work complete and validation passed",
149
+ codexGoalJson: snapshot("complete"),
150
+ });
151
+ expect(result.nextActions.join(" ")).toContain("create_goal");
158
152
  });
159
153
  });
@@ -128,6 +128,33 @@ describe("ulwLoopCommand checkpoint", () => {
128
128
  expect(stdoutJson()).toHaveProperty("goal.status", "complete");
129
129
  });
130
130
 
131
+ it("#given a complete checkpoint with an objective difference #when invoked with JSON #then exposes driver advice and warning", async () => {
132
+ await createPlan();
133
+ await passCriterion("G001-goal-a", "C001");
134
+ await passCriterion("G001-goal-a", "C002");
135
+ await passCriterion("G001-goal-a", "C003");
136
+
137
+ expect(
138
+ await ulwLoopCommand([
139
+ "checkpoint",
140
+ "--goal-id",
141
+ "G001-goal-a",
142
+ "--status",
143
+ "complete",
144
+ "--evidence",
145
+ "implementation done and validation passed",
146
+ "--codex-goal-json",
147
+ JSON.stringify({ goal: { objective: "different driver", status: "budget_limited" } }),
148
+ "--json",
149
+ ]),
150
+ ).toBe(0);
151
+ const result = stdoutJson();
152
+ expect(result).toMatchObject({ ok: true });
153
+ expect(result).toHaveProperty("nextActions");
154
+ expect(JSON.stringify(result)).toContain("driver_objective_differs");
155
+ expect(JSON.stringify(result)).toContain("/goal resume");
156
+ });
157
+
131
158
  it("ACCEPTS when all criteria pass", async () => {
132
159
  await createPlan();
133
160
  await passCriterion("G001-goal-a", "C001");
@@ -78,6 +78,31 @@ describe("ulwLoopCommand --json error contract", () => {
78
78
  expect(stdoutJson()).toMatchObject({ ok: false, error: { code: expect.any(String) } });
79
79
  });
80
80
 
81
+ it("#given an invalid codex snapshot token #when checkpoint --json #then emits the stable typed error", async () => {
82
+ const code = await ulwLoopCommand([
83
+ "checkpoint",
84
+ "--json",
85
+ "--goal-id",
86
+ "G001",
87
+ "--status",
88
+ "complete",
89
+ "--evidence",
90
+ "proof",
91
+ "--codex-goal-json",
92
+ "not-json-not-a-path",
93
+ ]);
94
+
95
+ expect(code).toBe(1);
96
+ expect(err.join("")).toBe("");
97
+ expect(stdoutJson()).toMatchObject({
98
+ ok: false,
99
+ error: {
100
+ code: "ULW_LOOP_CODEX_GOAL_JSON_INVALID",
101
+ message: expect.stringContaining("neither valid JSON nor a readable path"),
102
+ },
103
+ });
104
+ });
105
+
81
106
  it("#given a malformed required flag #when --json #then surfaces the UlwLoopError code with details on stdout", async () => {
82
107
  const code = await ulwLoopCommand(["criteria", "--json"]);
83
108
 
@@ -103,10 +103,10 @@ describe("readCodexGoalSnapshotInput", () => {
103
103
 
104
104
  it("reads from sample fixture path", async () => {
105
105
  // given
106
- const filePath = join(process.cwd(), "test", "fixtures", "codex-goal-snapshot.json");
106
+ const filePath = new URL("./fixtures/codex-goal-snapshot.json", import.meta.url);
107
107
 
108
108
  // when
109
- const snapshot = await readCodexGoalSnapshotInput(filePath);
109
+ const snapshot = await readCodexGoalSnapshotInput(filePath.pathname);
110
110
 
111
111
  // then
112
112
  expect(snapshot?.available).toBe(true);
@@ -120,6 +120,15 @@ describe("readCodexGoalSnapshotInput", () => {
120
120
  });
121
121
 
122
122
  describe("reconcileCodexGoalSnapshot", () => {
123
+ it("preserves objective whitespace in driver advice while normalizing comparison", () => {
124
+ const expectedObjective = " exact objective\nwith spacing ";
125
+ const result = reconcileCodexGoalSnapshot(
126
+ parseCodexGoalSnapshot({ goal: { objective: "different", status: "active" } }),
127
+ { expectedObjective },
128
+ );
129
+
130
+ expect(result.warnings[0]).toContain(`expected "${expectedObjective}"`);
131
+ });
123
132
  it("returns ok=true when snapshot matches expected", () => {
124
133
  // when
125
134
  const reconciliation = reconcileCodexGoalSnapshot(
@@ -132,7 +141,7 @@ describe("reconcileCodexGoalSnapshot", () => {
132
141
  expect(reconciliation.errors).toHaveLength(0);
133
142
  });
134
143
 
135
- it("reports error when objective mismatches", () => {
144
+ it("reports warning when objective mismatches", () => {
136
145
  // when
137
146
  const reconciliation = reconcileCodexGoalSnapshot(
138
147
  { available: true, objective: "X", status: "active", raw: null },
@@ -140,20 +149,34 @@ describe("reconcileCodexGoalSnapshot", () => {
140
149
  );
141
150
 
142
151
  // then
143
- expect(reconciliation.ok).toBe(false);
144
- expect(reconciliation.errors.length).toBeGreaterThan(0);
152
+ expect(reconciliation.ok).toBe(true);
153
+ expect(reconciliation.errors).toHaveLength(0);
154
+ expect(reconciliation.warnings.join(" ")).toContain("driver_objective_differs");
145
155
  });
146
156
 
147
- it("reports error when status mismatches", () => {
157
+ it("accepts a parsable snapshot without objective as advisory", () => {
148
158
  // when
149
159
  const reconciliation = reconcileCodexGoalSnapshot(
150
- { available: true, objective: "X", status: "active", raw: null },
151
- { expectedObjective: "X", allowedStatuses: ["complete"] },
160
+ { available: true, status: "active", raw: { goal: { status: "active" } } },
161
+ { expectedObjective: "X" },
152
162
  );
153
163
 
154
164
  // then
155
- expect(reconciliation.ok).toBe(false);
156
- expect(reconciliation.errors.length).toBeGreaterThan(0);
165
+ expect(reconciliation.ok).toBe(true);
166
+ expect(reconciliation.errors).toHaveLength(0);
167
+ });
168
+
169
+ it("accepts limited driver statuses", () => {
170
+ // when
171
+ const reconciliation = reconcileCodexGoalSnapshot(
172
+ { available: true, objective: "X", status: "budget_limited", raw: null },
173
+ { expectedObjective: "X" },
174
+ );
175
+
176
+ // then
177
+ expect(reconciliation.ok).toBe(true);
178
+ expect(reconciliation.errors).toHaveLength(0);
179
+ expect(reconciliation.warnings.join(" ")).toContain("/goal resume");
157
180
  });
158
181
  });
159
182
 
@@ -162,7 +185,7 @@ describe("formatCodexGoalReconciliation", () => {
162
185
  // given
163
186
  const reconciliation = reconcileCodexGoalSnapshot(
164
187
  { available: true, objective: "X", status: "active", raw: null },
165
- { expectedObjective: "Y", allowedStatuses: ["complete"] },
188
+ { expectedObjective: "Y" },
166
189
  );
167
190
 
168
191
  // when
@@ -4,12 +4,10 @@ import { join } from "node:path";
4
4
  import { describe, expect, it } from "vitest";
5
5
 
6
6
  import { checkpointUlwLoop } from "../src/checkpoint.js";
7
- import { ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js";
8
7
  import { ulwLoopDir } from "../src/paths.js";
9
8
  import { readUlwLoopPlan } from "../src/plan-io.js";
10
- import { recordFinalReviewBlockers } from "../src/review-blockers.js";
11
9
  import { UlwLoopError } from "../src/types.js";
12
- import { goal, passGoal, plan, repoWith, snapshot } from "./fixtures/checkpoint-builders.js";
10
+ import { goal, passGoal, plan, repoWith } from "./fixtures/checkpoint-builders.js";
13
11
 
14
12
  async function captureError(action: () => Promise<unknown>): Promise<UlwLoopError> {
15
13
  try {
@@ -23,52 +21,16 @@ async function captureError(action: () => Promise<unknown>): Promise<UlwLoopErro
23
21
  }
24
22
 
25
23
  describe("#given a codex goal snapshot whose objective differs from the plan", () => {
26
- it("#when checkpoint reconciles it #then the mismatch carries the verbatim expected and received objectives", async () => {
24
+ it("#when checkpoint reconciles it #then it succeeds with driver advice", async () => {
27
25
  const repo = await repoWith(plan([passGoal("G001"), goal({ id: "G002", status: "pending" })]));
28
-
29
- const error = await captureError(() =>
30
- checkpointUlwLoop(repo, {
31
- goalId: "G001",
32
- status: "complete",
33
- evidence: "work complete and validation passed",
34
- codexGoalJson: snapshot("active", "wrong objective"),
35
- }),
36
- );
37
-
38
- expect(error.code).toBe("ulw_loop_codex_snapshot_mismatch");
39
- expect(error.details).toMatchObject({
40
- expectedObjective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE,
41
- receivedObjective: "wrong objective",
42
- });
43
- expect(error.message).toContain(
44
- "objective must equal the plan's codexObjective exactly — copy the expected value below",
45
- );
46
- expect(error.message).toContain(ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE);
47
- });
48
-
49
- it("#when record-review-blockers reconciles it #then the same guided payload is emitted", async () => {
50
- const repo = await repoWith(
51
- plan([passGoal("G001", { status: "complete" }), goal({ id: "G002", status: "in_progress" })]),
52
- );
53
-
54
- const error = await captureError(() =>
55
- recordFinalReviewBlockers(repo, {
56
- goalId: "G002",
57
- title: "Resolve final review blockers",
58
- objective: "Address the BLOCK findings",
59
- evidence: "review verdict: REQUEST_CHANGES",
60
- codexGoalJson: JSON.stringify({ goal: { objective: "stale objective", status: "active" } }),
61
- }),
62
- );
63
-
64
- expect(error.code).toBe("ulw_loop_codex_snapshot_mismatch");
65
- expect(error.details).toMatchObject({
66
- expectedObjective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE,
67
- receivedObjective: "stale objective",
26
+ const result = await checkpointUlwLoop(repo, {
27
+ goalId: "G001",
28
+ status: "complete",
29
+ evidence: "work complete",
30
+ codexGoalJson: JSON.stringify({ goal: { objective: "wrong objective", status: "budget_limited" } }),
68
31
  });
69
- expect(error.message).toContain(
70
- "objective must equal the plan's codexObjective exactly — copy the expected value below",
71
- );
32
+ expect(result.nextActions.join(" ")).toContain("/goal resume");
33
+ expect(result.warnings.join(" ")).toContain("driver_objective_differs");
72
34
  });
73
35
  });
74
36
 
@@ -157,15 +157,17 @@ describe("recordFinalReviewBlockers error cases", () => {
157
157
  "ulw_loop_not_final_story",
158
158
  );
159
159
  });
160
+ });
160
161
 
161
- it("throws ulw_loop_codex_snapshot_mismatch when objective mismatches", async () => {
162
+ describe("recordFinalReviewBlockers advisory results", () => {
163
+ it("returns objective-difference warning and resume action for a limited snapshot", async () => {
162
164
  const repo = await bootstrapRepo(finalPlan());
163
- const codexGoalJson = JSON.stringify({ goal: { objective: "wrong", status: "active" } });
164
-
165
- await expectUlwLoopCode(
166
- () => recordFinalReviewBlockers(repo, { ...validArgs, codexGoalJson }),
167
- "ulw_loop_codex_snapshot_mismatch",
168
- );
165
+ const result = await recordFinalReviewBlockers(repo, {
166
+ ...validArgs,
167
+ codexGoalJson: JSON.stringify({ goal: { objective: "different", status: "budget_limited" } }),
168
+ });
169
+ expect(result.nextActions).toContain("/goal resume or raise the budget");
170
+ expect(result.warnings.some((warning) => warning.startsWith("driver_objective_differs"))).toBe(true);
169
171
  });
170
172
  });
171
173
 
@@ -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.53) Resetting Git Bash MCP Reminder",
10
+ "statusMessage": "(OmO 5.0.0-beta.55) 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.53) Resetting LSP Diagnostics Cache",
10
+ "statusMessage": "(OmO 5.0.0-beta.55) 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.53) Resetting Project Rule Cache",
10
+ "statusMessage": "(OmO 5.0.0-beta.55) 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.53) Checking Comments",
10
+ "statusMessage": "(OmO 5.0.0-beta.55) 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
  ],