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
@@ -7,16 +7,25 @@ function safeObject(value) {
7
7
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
8
8
  }
9
9
  function safeString(value) {
10
+ return typeof value === "string" ? value : "";
11
+ }
12
+ function safeStatusString(value) {
10
13
  return typeof value === "string" ? value.trim() : "";
11
14
  }
12
15
  function normalizeStatus(value) {
13
- const status = safeString(value).toLowerCase();
16
+ const status = safeStatusString(value).toLowerCase();
14
17
  if (status === "complete" || status === "completed" || status === "done")
15
18
  return "complete";
16
19
  if (status === "cancelled" || status === "canceled")
17
20
  return "cancelled";
18
21
  if (status === "failed" || status === "failure")
19
22
  return "failed";
23
+ if (status === "paused")
24
+ return "paused";
25
+ if (status === "usage_limited")
26
+ return "usage_limited";
27
+ if (status === "budget_limited")
28
+ return "budget_limited";
20
29
  if (status === "active" || status === "in_progress" || status === "pending" || status === "running")
21
30
  return "active";
22
31
  return "unknown";
@@ -64,48 +73,29 @@ export function reconcileCodexGoalSnapshot(snapshot, options) {
64
73
  const effectiveSnapshot = snapshot ?? { available: false, raw: null };
65
74
  const errors = [];
66
75
  const warnings = [];
76
+ const expected = options.expectedObjective;
77
+ const normalizedExpected = normalizeObjective(expected);
67
78
  if (!effectiveSnapshot.available) {
68
- const message = "Codex goal snapshot is absent or reports no active goal; call get_goal and pass its JSON with --codex-goal-json.";
69
- if (options.requireSnapshot)
70
- errors.push(message);
71
- else
72
- warnings.push(message);
79
+ warnings.push(`call get_goal; if none, create_goal with codexObjective "${expected}" verbatim`);
73
80
  return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
74
81
  }
75
- const expected = normalizeObjective(options.expectedObjective);
76
- const accepted = new Set([expected, ...(options.acceptedObjectives ?? []).map((objective) => normalizeObjective(objective))].filter(Boolean));
82
+ const accepted = new Set([
83
+ normalizedExpected,
84
+ ...(options.acceptedObjectives ?? []).map((objective) => normalizeObjective(objective)),
85
+ ].filter(Boolean));
77
86
  const actual = normalizeObjective(effectiveSnapshot.objective ?? "");
78
- if (!actual) {
79
- errors.push("Codex goal snapshot is missing objective text.");
80
- }
81
- else if (!accepted.has(actual)) {
82
- errors.push(`Codex goal objective mismatch: expected "${expected}", got "${actual}".`);
87
+ if (actual && !accepted.has(normalizeObjective(actual))) {
88
+ warnings.push(`driver_objective_differs: expected "${expected}", got "${actual}".`);
83
89
  }
84
- const allowed = options.allowedStatuses ?? (options.requireComplete ? ["complete"] : ["active", "complete"]);
85
90
  const actualStatus = effectiveSnapshot.status ?? "unknown";
86
- if (!allowed.includes(actualStatus)) {
87
- errors.push(`Codex goal status mismatch: expected ${allowed.join(" or ")}, got ${actualStatus}.`);
88
- }
89
- if (options.requireComplete && actualStatus !== "complete") {
90
- errors.push('Codex goal is not complete; call update_goal({status: "complete"}) only after the objective is actually complete, then pass the fresh get_goal JSON.');
91
+ if (actualStatus === "paused" || actualStatus === "usage_limited" || actualStatus === "budget_limited") {
92
+ warnings.push("/goal resume or raise the budget");
91
93
  }
94
+ if (actualStatus === "complete")
95
+ warnings.push(`driver closed early: call create_goal with codexObjective "${expected}" verbatim`);
92
96
  return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
93
97
  }
94
98
  export function formatCodexGoalReconciliation(reconciliation) {
95
99
  const parts = [...reconciliation.errors, ...reconciliation.warnings];
96
100
  return parts.join(" ");
97
101
  }
98
- /**
99
- * The reconciliation errors normalize whitespace for comparison, which makes the
100
- * quoted objective unusable as a copy source. Recovery therefore carries the
101
- * plan's `codexObjective` verbatim so the agent can paste it into `update_goal`.
102
- */
103
- export function codexGoalMismatchRecovery(expectedObjective, snapshot) {
104
- const receivedObjective = snapshot?.objective ?? "";
105
- const message = [
106
- "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.",
107
- `expected codexObjective: ${expectedObjective}`,
108
- `received objective: ${receivedObjective || "(none)"}`,
109
- ].join("\n");
110
- return { message, details: { expectedObjective, receivedObjective } };
111
- }
@@ -12,5 +12,7 @@ export interface RecordFinalReviewBlockersResult {
12
12
  readonly blockedGoal: UlwLoopItem;
13
13
  readonly newGoal: UlwLoopItem;
14
14
  readonly ledgerEntries: UlwLoopLedgerEntry[];
15
+ readonly nextActions: readonly string[];
16
+ readonly warnings: readonly string[];
15
17
  }
16
18
  export declare function recordFinalReviewBlockers(repoRoot: string, args: RecordFinalReviewBlockersArgs, scope?: UlwLoopScope): Promise<RecordFinalReviewBlockersResult>;
@@ -1,7 +1,6 @@
1
1
  // biome-ignore-all format: compact port must stay within the requested pure LOC budget.
2
- import { codexSnapshotMismatchError } from "./checkpoint-reconciliation.js";
3
- import { readCodexGoalSnapshotInput, reconcileCodexGoalSnapshot } from "./codex-goal-snapshot.js";
4
- import { codexGoalMode, compatibleCodexObjectives, expectedCodexObjective, isFinalRunCompletionCandidate } from "./goal-status.js";
2
+ import { CodexGoalSnapshotError, formatCodexGoalReconciliation, readCodexGoalSnapshotInput, reconcileCodexGoalSnapshot, } from "./codex-goal-snapshot.js";
3
+ import { compatibleCodexObjectives, expectedCodexObjective, isFinalRunCompletionCandidate } from "./goal-status.js";
5
4
  import { seedDefaultSuccessCriteria } from "./plan-crud.js";
6
5
  import { appendLedger, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
7
6
  import { iso, UlwLoopError } from "./types.js";
@@ -42,11 +41,12 @@ export async function recordFinalReviewBlockers(repoRoot, args, scope) {
42
41
  if (!isFinalRunCompletionCandidate(plan, goal))
43
42
  ulwLoopError(`${goal.id} is not final.`, "ulw_loop_not_final_story");
44
43
  const snapshot = await readCodexGoalSnapshotInput(args.codexGoalJson, repoRoot);
45
- const aggregate = codexGoalMode(plan) === "aggregate";
46
- const expected = expectedCodexObjective(plan, goal);
47
- const reconciliation = reconcileCodexGoalSnapshot(snapshot, { expectedObjective: expected, ...(aggregate ? { acceptedObjectives: compatibleCodexObjectives(plan) } : {}), allowedStatuses: ["active"], requireSnapshot: true, requireComplete: false });
44
+ const reconciliation = reconcileCodexGoalSnapshot(snapshot, {
45
+ expectedObjective: expectedCodexObjective(plan, goal),
46
+ acceptedObjectives: compatibleCodexObjectives(plan),
47
+ });
48
48
  if (!reconciliation.ok)
49
- throw codexSnapshotMismatchError({ reconciliation, snapshot, expectedObjective: expected });
49
+ throw new CodexGoalSnapshotError(formatCodexGoalReconciliation(reconciliation));
50
50
  const now = iso();
51
51
  for (const field of BLOCKER_FIELDS)
52
52
  Reflect.deleteProperty(goal, field);
@@ -58,7 +58,7 @@ export async function recordFinalReviewBlockers(repoRoot, args, scope) {
58
58
  delete plan.activeGoalId;
59
59
  const newGoal = appendBlockerGoal(plan, args, now);
60
60
  plan.updatedAt = now;
61
- const codexGoal = reconciliation.snapshot.raw;
61
+ const codexGoal = snapshot?.raw;
62
62
  const blockedEntry = { at: now, kind: "goal_review_blocked", goalId: goal.id, status: goal.status, evidence: args.evidence, codexGoal };
63
63
  const addedEntry = { at: now, kind: "goal_added", goalId: newGoal.id, status: newGoal.status, evidence: args.evidence, message: newGoal.title };
64
64
  const summaryEntry = { at: now, kind: "goal_review_blocked", goalId: goal.id, status: goal.status, evidence: args.evidence, codexGoal, message: `Review blockers recorded; appended ${newGoal.id}.` };
@@ -67,6 +67,13 @@ export async function recordFinalReviewBlockers(repoRoot, args, scope) {
67
67
  await writePlan(repoRoot, plan, scope);
68
68
  for (const entry of ledgerEntries)
69
69
  await appendLedger(repoRoot, entry, scope);
70
- return { plan, blockedGoal: goal, newGoal, ledgerEntries };
70
+ return {
71
+ plan,
72
+ blockedGoal: goal,
73
+ newGoal,
74
+ ledgerEntries,
75
+ nextActions: reconciliation.warnings,
76
+ warnings: reconciliation.warnings.filter((warning) => warning.startsWith("driver_objective_differs")),
77
+ };
71
78
  });
72
79
  }
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit --with-ultrawork",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.53) Checking Ulw-Loop Steering"
10
+ "statusMessage": "(OmO 5.0.0-beta.55) Checking Ulw-Loop Steering"
11
11
  }
12
12
  ]
13
13
  }
@@ -20,7 +20,7 @@
20
20
  "type": "command",
21
21
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use-spawn",
22
22
  "timeout": 5,
23
- "statusMessage": "(OmO 5.0.0-beta.53) Recording Spawn Admission"
23
+ "statusMessage": "(OmO 5.0.0-beta.55) Recording Spawn Admission"
24
24
  }
25
25
  ]
26
26
  }
@@ -33,7 +33,7 @@
33
33
  "type": "command",
34
34
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
35
35
  "timeout": 5,
36
- "statusMessage": "(OmO 5.0.0-beta.53) Enforcing Unlimited Ulw-Loop Budget"
36
+ "statusMessage": "(OmO 5.0.0-beta.55) Enforcing Unlimited Ulw-Loop Budget"
37
37
  }
38
38
  ]
39
39
  },
@@ -44,7 +44,7 @@
44
44
  "type": "command",
45
45
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use-spawn",
46
46
  "timeout": 5,
47
- "statusMessage": "(OmO 5.0.0-beta.53) Guarding Ulw-Loop Spawns"
47
+ "statusMessage": "(OmO 5.0.0-beta.55) Guarding Ulw-Loop Spawns"
48
48
  }
49
49
  ]
50
50
  }
@@ -56,7 +56,7 @@
56
56
  "type": "command",
57
57
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook stop",
58
58
  "timeout": 10,
59
- "statusMessage": "(OmO 5.0.0-beta.53) Checking Ulw-Loop Resume"
59
+ "statusMessage": "(OmO 5.0.0-beta.55) Checking Ulw-Loop Resume"
60
60
  }
61
61
  ]
62
62
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-ulw-loop",
3
- "version": "5.0.0-beta.53",
3
+ "version": "5.0.0-beta.55",
4
4
  "description": "Codex plugin: durable repo-native multi-goal orchestration with embedded success criteria and observable evidence audit.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -1,23 +1,14 @@
1
1
  import {
2
- canReconcileActiveFinalTaskScopedAggregateSnapshot,
3
- canReconcileCompletedTaskScopedAggregateSnapshot,
4
- codexSnapshotMismatchError,
5
- } from "./checkpoint-reconciliation.js";
6
- import { readCodexGoalSnapshotInput, reconcileCodexGoalSnapshot } from "./codex-goal-snapshot.js";
7
- import {
8
- codexGoalMode,
9
- compatibleCodexObjectives,
10
- expectedCodexObjective,
11
- isFinalRunCompletionCandidate,
12
- } from "./goal-status.js";
2
+ CodexGoalSnapshotError,
3
+ formatCodexGoalReconciliation,
4
+ readCodexGoalSnapshotInput,
5
+ reconcileCodexGoalSnapshot,
6
+ } from "./codex-goal-snapshot.js";
7
+ import { codexGoalMode, compatibleCodexObjectives, expectedCodexObjective } from "./goal-status.js";
13
8
  import type { UlwLoopScope } from "./paths.js";
14
9
  import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
15
10
  import { UlwLoopError } from "./types.js";
16
11
 
17
- function normalizeObjective(value: string): string {
18
- return value.replace(/\s+/g, " ").trim();
19
- }
20
-
21
12
  export async function validateCheckpointCodexGoal(input: {
22
13
  readonly repoRoot: string;
23
14
  readonly plan: UlwLoopPlan;
@@ -25,53 +16,21 @@ export async function validateCheckpointCodexGoal(input: {
25
16
  readonly raw: string | undefined;
26
17
  readonly evidence: string;
27
18
  readonly scope?: UlwLoopScope;
28
- }): Promise<unknown> {
29
- const aggregate = codexGoalMode(input.plan) === "aggregate";
30
- const final = isFinalRunCompletionCandidate(input.plan, input.goal);
19
+ }): Promise<{ readonly raw: unknown; readonly nextActions: readonly string[]; readonly warnings: readonly string[] }> {
31
20
  const snapshot = await readCodexGoalSnapshotInput(input.raw, input.repoRoot);
32
- const expectedObjective = expectedCodexObjective(input.plan, input.goal);
21
+ const expected = expectedCodexObjective(input.plan, input.goal);
33
22
  const reconciliation = reconcileCodexGoalSnapshot(snapshot, {
34
- expectedObjective,
35
- ...(aggregate ? { acceptedObjectives: compatibleCodexObjectives(input.plan) } : {}),
36
- allowedStatuses: aggregate ? (final ? ["complete"] : ["active"]) : ["complete"],
37
- requireSnapshot: true,
38
- requireComplete: !aggregate || final,
39
- });
40
- if (reconciliation.ok) return reconciliation.snapshot.raw;
41
- const objective = snapshot?.objective;
42
- const mismatchedTaskObjective =
43
- snapshot?.available === true &&
44
- objective !== undefined &&
45
- normalizeObjective(objective) !== normalizeObjective(expectedObjective);
46
- const completedTaskScoped =
47
- mismatchedTaskObjective &&
48
- snapshot.status === "complete" &&
49
- (await canReconcileCompletedTaskScopedAggregateSnapshot(
50
- input.repoRoot,
51
- input.plan,
52
- input.goal,
53
- objective,
54
- input.evidence,
55
- input.scope,
56
- ));
57
- const activeFinalTaskScoped =
58
- mismatchedTaskObjective &&
59
- snapshot.status === "active" &&
60
- (await canReconcileActiveFinalTaskScopedAggregateSnapshot(
61
- input.repoRoot,
62
- input.plan,
63
- input.goal,
64
- objective,
65
- input.evidence,
66
- input.scope,
67
- ));
68
- if (completedTaskScoped || activeFinalTaskScoped) return reconciliation.snapshot.raw;
69
- throw codexSnapshotMismatchError({
70
- reconciliation,
71
- snapshot,
72
- expectedObjective,
73
- taskScopedHint: { goal: input.goal, aggregate, final },
23
+ expectedObjective: expected,
24
+ ...(codexGoalMode(input.plan) === "aggregate"
25
+ ? { acceptedObjectives: compatibleCodexObjectives(input.plan) }
26
+ : {}),
74
27
  });
28
+ if (!reconciliation.ok) throw new CodexGoalSnapshotError(formatCodexGoalReconciliation(reconciliation));
29
+ return {
30
+ raw: snapshot?.raw,
31
+ nextActions: reconciliation.warnings,
32
+ warnings: reconciliation.warnings.filter((warning) => warning.startsWith("driver_objective_differs")),
33
+ };
75
34
  }
76
35
 
77
36
  export function combineCheckpointValidationErrors(codexError: UlwLoopError, gateError: UlwLoopError): UlwLoopError {
@@ -42,12 +42,7 @@ export async function checkpoint(
42
42
  const goalId = required(argv, "--goal-id");
43
43
  const statusValue = checkpointStatus(required(argv, "--status"));
44
44
  const evidence = required(argv, "--evidence");
45
- const codexGoalJson = await parseCodexGoalJson(
46
- statusValue === "complete" ? required(argv, "--codex-goal-json") : readValue(argv, "--codex-goal-json"),
47
- );
48
- if (statusValue === "complete" && codexGoalJson === undefined) {
49
- throw new UlwLoopError("Missing --codex-goal-json.", "ULW_LOOP_CODEX_GOAL_JSON_REQUIRED");
50
- }
45
+ const codexGoalJson = await parseCodexGoalJson(readValue(argv, "--codex-goal-json"));
51
46
  const qualityGateJson = readValue(argv, "--quality-gate-json");
52
47
  const args: CheckpointUlwLoopArgs & { readonly advance: boolean } = {
53
48
  goalId,
@@ -1,130 +1,7 @@
1
- import { existsSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
-
4
- import type { CodexGoalReconciliation, CodexGoalSnapshot } from "./codex-goal-snapshot.js";
5
- import { codexGoalMismatchRecovery, formatCodexGoalReconciliation } from "./codex-goal-snapshot.js";
6
- import { codexGoalMode, isFinalRunCompletionCandidate } from "./goal-status.js";
7
- import { type UlwLoopScope, ulwLoopBriefPath } from "./paths.js";
8
- import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
9
- import { ULW_LOOP_DIR, ULW_LOOP_GOALS, ULW_LOOP_LEDGER, UlwLoopError } from "./types.js";
10
-
11
- export interface CodexSnapshotMismatchInput {
12
- readonly reconciliation: CodexGoalReconciliation;
13
- readonly snapshot: CodexGoalSnapshot | null | undefined;
14
- readonly expectedObjective: string;
15
- /** Only the checkpoint path can fall back to task-scoped aggregate reconciliation, so only it asks for that hint. */
16
- readonly taskScopedHint?: { readonly goal: UlwLoopItem; readonly aggregate: boolean; readonly final: boolean };
17
- }
1
+ import { UlwLoopError } from "./types.js";
18
2
 
19
3
  export function combineCheckpointValidationErrors(codexError: UlwLoopError, gateError: UlwLoopError): UlwLoopError {
20
4
  return new UlwLoopError(`${codexError.message}\n${gateError.message}`, "ULW_LOOP_QUALITY_GATE_INVALID", {
21
5
  details: { ...(codexError.details ?? {}), ...(gateError.details ?? {}) },
22
6
  });
23
7
  }
24
-
25
- export function codexSnapshotMismatchError(input: CodexSnapshotMismatchInput): UlwLoopError {
26
- const { reconciliation, snapshot, taskScopedHint } = input;
27
- const hint =
28
- taskScopedHint?.aggregate === true && snapshot?.status === "complete" && snapshot.objective !== undefined
29
- ? buildTaskScopedAggregateReconciliationHint(taskScopedHint.goal, taskScopedHint.final)
30
- : "";
31
- const recovery = codexGoalMismatchRecovery(input.expectedObjective, snapshot);
32
- return new UlwLoopError(
33
- `${formatCodexGoalReconciliation(reconciliation)}${hint}\n${recovery.message}`,
34
- "ulw_loop_codex_snapshot_mismatch",
35
- { details: recovery.details },
36
- );
37
- }
38
-
39
- function normalizeObjective(value: string): string {
40
- return value.replace(/\s+/g, " ").trim();
41
- }
42
-
43
- function textMentionsUlwLoopPlanArtifact(value: string | undefined): boolean {
44
- const normalized = (value ?? "").toLowerCase();
45
- return (
46
- normalized.includes(ULW_LOOP_DIR.toLowerCase()) ||
47
- normalized.includes(ULW_LOOP_GOALS.toLowerCase()) ||
48
- normalized.includes(ULW_LOOP_LEDGER.toLowerCase())
49
- );
50
- }
51
-
52
- function textMentionsGoalId(value: string | undefined, goalId: string): boolean {
53
- return (value ?? "").toLowerCase().includes(goalId.toLowerCase());
54
- }
55
-
56
- function textHasCompletionValidationEvidence(value: string | undefined): boolean {
57
- const normalized = (value ?? "").toLowerCase();
58
- const done =
59
- /\b(?:planned work|implementation|deliverables?|scope|task|work)\b/.test(normalized) &&
60
- /\b(?:done|complete|completed|finished|shipped)\b/.test(normalized);
61
- const verified =
62
- /\b(?:validation|verification|tests?|build|lint|review|quality gate|code-review)\b/.test(normalized) &&
63
- /\b(?:passed|complete|completed|clean|green|approve|approved|clear)\b/.test(normalized);
64
- return done && verified;
65
- }
66
-
67
- async function snapshotObjectiveMapsToUlwLoopPlan(
68
- repoRoot: string,
69
- snapshotObjective: string,
70
- scope?: UlwLoopScope,
71
- ): Promise<boolean> {
72
- const actual = normalizeObjective(snapshotObjective).toLowerCase();
73
- if (textMentionsUlwLoopPlanArtifact(actual)) return true;
74
- if (actual.length < 24 || !existsSync(ulwLoopBriefPath(repoRoot, scope))) return false;
75
- try {
76
- const brief = normalizeObjective(await readFile(ulwLoopBriefPath(repoRoot, scope), "utf8")).toLowerCase();
77
- return brief.length >= 24 && (brief.includes(actual) || actual.includes(brief));
78
- } catch (error) {
79
- if (error instanceof Error) return false;
80
- throw error;
81
- }
82
- }
83
-
84
- export async function canReconcileCompletedTaskScopedAggregateSnapshot(
85
- repoRoot: string,
86
- plan: UlwLoopPlan,
87
- goal: UlwLoopItem,
88
- snapshotObjective: string,
89
- evidence: string,
90
- scope?: UlwLoopScope,
91
- ): Promise<boolean> {
92
- if (codexGoalMode(plan) !== "aggregate") return false;
93
- if (goal.status !== "in_progress" || plan.activeGoalId !== goal.id) return false;
94
- if (isFinalRunCompletionCandidate(plan, goal)) {
95
- return snapshotObjectiveMapsToUlwLoopPlan(repoRoot, snapshotObjective, scope);
96
- }
97
- if (!textMentionsUlwLoopPlanArtifact(evidence) || !textMentionsGoalId(evidence, goal.id)) return false;
98
- if (!textHasCompletionValidationEvidence(evidence)) return false;
99
- return snapshotObjectiveMapsToUlwLoopPlan(repoRoot, snapshotObjective, scope);
100
- }
101
-
102
- export async function canReconcileActiveFinalTaskScopedAggregateSnapshot(
103
- repoRoot: string,
104
- plan: UlwLoopPlan,
105
- goal: UlwLoopItem,
106
- snapshotObjective: string,
107
- evidence: string,
108
- scope?: UlwLoopScope,
109
- ): Promise<boolean> {
110
- if (codexGoalMode(plan) !== "aggregate") return false;
111
- if (goal.status !== "in_progress" || plan.activeGoalId !== goal.id) return false;
112
- if (!isFinalRunCompletionCandidate(plan, goal)) return false;
113
- if (!textHasCompletionValidationEvidence(evidence)) return false;
114
- return snapshotObjectiveMapsToUlwLoopPlan(repoRoot, snapshotObjective, scope);
115
- }
116
-
117
- function buildCompletedLegacyGoalRemediation(goal: UlwLoopItem): string {
118
- return [
119
- "If get_goal returns a different completed legacy/thread objective, do not repeat --status complete in this thread.",
120
- `Record a non-terminal blocker with: omo-agent-toolkit ulw-loop checkpoint --goal-id ${goal.id} --status blocked --evidence "<completed legacy Codex goal blocks create_goal in this thread>" --codex-goal-json "<different completed get_goal JSON or path>".`,
121
- "Then continue only from a Codex goal context with no active/completed conflicting goal, in the same repo/worktree, and create the intended goal there.",
122
- ].join(" ");
123
- }
124
-
125
- export function buildTaskScopedAggregateReconciliationHint(goal: UlwLoopItem, final: boolean): string {
126
- if (final) {
127
- return ` Final task-scoped aggregate reconciliation requires the checkpoint goal to be the active in-progress final OMO goal and the completed get_goal objective to map to the ulw-loop brief or artifact. ${buildCompletedLegacyGoalRemediation(goal)}`;
128
- }
129
- return ` Completed task-scoped aggregate reconciliation requires the checkpoint goal to be the active in-progress OMO goal, evidence that names that active OMO goal id, names .omo/ulw-loop/goals.json or ledger.jsonl, includes completed implementation plus validation/review evidence, and a get_goal objective that maps to the ulw-loop brief/artifact. ${buildCompletedLegacyGoalRemediation(goal)}`;
130
- }
@@ -40,6 +40,8 @@ export interface CheckpointUlwLoopResult {
40
40
  readonly goal: UlwLoopItem;
41
41
  readonly ledgerEntry: UlwLoopLedgerEntry;
42
42
  readonly aggregateCompletion?: UlwLoopAggregateCompletion;
43
+ readonly nextActions: readonly string[];
44
+ readonly warnings: readonly string[];
43
45
  }
44
46
 
45
47
  const QUALITY_GATE_FS = { existsSync, statSync } as const;
@@ -158,6 +160,8 @@ export async function checkpointUlwLoop(
158
160
  let aggregateCompletion: UlwLoopAggregateCompletion | undefined;
159
161
  let qualityGate: UlwLoopQualityGate | undefined;
160
162
  let codexGoal: unknown;
163
+ let nextActions: readonly string[] = [];
164
+ let warnings: readonly string[] = [];
161
165
  if (args.status === "complete") {
162
166
  const aggregate = codexGoalMode(plan) === "aggregate";
163
167
  const final = isFinalRunCompletionCandidate(plan, goal);
@@ -170,7 +174,7 @@ export async function checkpointUlwLoop(
170
174
  else requireAllCriteriaPass(goal);
171
175
  let codexValidationError: UlwLoopError | undefined;
172
176
  try {
173
- codexGoal = await validateCheckpointCodexGoal({
177
+ const validation = await validateCheckpointCodexGoal({
174
178
  repoRoot,
175
179
  plan,
176
180
  goal,
@@ -178,6 +182,9 @@ export async function checkpointUlwLoop(
178
182
  evidence,
179
183
  ...(scope === undefined ? {} : { scope }),
180
184
  });
185
+ codexGoal = validation.raw;
186
+ nextActions = validation.nextActions;
187
+ warnings = validation.warnings;
181
188
  } catch (error) {
182
189
  if (!(error instanceof UlwLoopError)) throw error;
183
190
  codexValidationError = error;
@@ -213,6 +220,7 @@ export async function checkpointUlwLoop(
213
220
  } else applyBlockedOrFailed(goal, plan, args.status, evidence, now);
214
221
  goal.updatedAt = now;
215
222
  if (aggregateCompletion !== undefined) plan.aggregateCompletion = aggregateCompletion;
223
+ if (aggregateCompletion !== undefined) nextActions = [...nextActions, 'aggregate complete — now update_goal({status:"complete"})'];
216
224
  plan.updatedAt = now;
217
225
  await writePlan(repoRoot, plan, scope);
218
226
  const ledgerEntry = buildLedger(now, args, goal, qualityGate, codexGoal, aggregateCompletion);
@@ -220,7 +228,7 @@ export async function checkpointUlwLoop(
220
228
  const closedBatch = args.status === "complete" ? batchClosedBy(plan, goal.id) : undefined;
221
229
  if (closedBatch !== undefined) await appendLedger(repoRoot, { at: now, kind: "batch_closed", goalId: goal.id, message: closedBatch.batchId }, scope);
222
230
  return aggregateCompletion === undefined
223
- ? { plan, goal, ledgerEntry }
224
- : { plan, goal, ledgerEntry, aggregateCompletion };
231
+ ? { plan, goal, ledgerEntry, nextActions, warnings }
232
+ : { plan, goal, ledgerEntry, aggregateCompletion, nextActions, warnings };
225
233
  });
226
234
  }
@@ -65,11 +65,17 @@ export async function readJsonInput(value: string | undefined): Promise<unknown
65
65
 
66
66
  export async function parseCodexGoalJson(value: string | undefined): Promise<string | undefined> {
67
67
  if (value === undefined) return undefined;
68
- const raw = looksLikeJson(value) ? value : await readFile(value, "utf8");
69
- try { JSON.parse(raw); return raw; }
70
- catch (error) {
68
+ try {
69
+ const raw = looksLikeJson(value) ? value : await readFile(value, "utf8");
70
+ JSON.parse(raw);
71
+ return raw;
72
+ } catch (error) {
71
73
  const message = error instanceof Error ? error.message : "unknown error";
72
- throw new UlwLoopError(`Invalid --codex-goal-json: ${message}`, "ULW_LOOP_CODEX_GOAL_JSON_INVALID", { cause: error });
74
+ throw new UlwLoopError(
75
+ `Invalid --codex-goal-json: ${looksLikeJson(value) ? message : "neither valid JSON nor a readable path"}`,
76
+ "ULW_LOOP_CODEX_GOAL_JSON_INVALID",
77
+ { cause: error },
78
+ );
73
79
  }
74
80
  }
75
81
 
@@ -203,6 +203,8 @@ export async function reviewBlockers(
203
203
  blockedGoal: result.blockedGoal,
204
204
  goal: result.newGoal,
205
205
  ledgerEntries: result.ledgerEntries,
206
+ nextActions: result.nextActions,
207
+ warnings: result.warnings,
206
208
  summary: summarizeUlwLoopPlan(result.plan),
207
209
  });
208
210
  } else {