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.
- package/dist/cli/index.js +53 -19
- package/dist/cli-node/index.js +53 -19
- package/package.json +1 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.d.ts +5 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.js +14 -34
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-continuation.js +1 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.d.ts +0 -18
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-reconciliation.js +1 -88
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.d.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +10 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-arg-parser.js +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +86 -174
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +2 -18
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +23 -33
- package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.d.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +16 -9
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-codex-validation.ts +18 -59
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-continuation.ts +1 -6
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-reconciliation.ts +1 -124
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +11 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-arg-parser.ts +10 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +31 -50
- package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +29 -10
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +0 -82
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint.test.ts +16 -22
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-checkpoint.test.ts +27 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/cli-json-errors.test.ts +25 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-snapshot.test.ts +34 -11
- package/packages/omo-codex/plugin/components/ulw-loop/test/guided-recovery.test.ts +9 -47
- package/packages/omo-codex/plugin/components/ulw-loop/test/review-blockers.test.ts +9 -7
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- 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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
[
|
|
114
|
-
|
|
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
|
-
|
|
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 (
|
|
127
|
-
|
|
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 {
|
|
4
|
-
|
|
5
|
-
|
|
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 {
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 =
|
|
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 {
|
|
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("
|
|
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
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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("
|
|
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
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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 =
|
|
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
|
|
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(
|
|
144
|
-
expect(reconciliation.errors
|
|
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("
|
|
157
|
+
it("accepts a parsable snapshot without objective as advisory", () => {
|
|
148
158
|
// when
|
|
149
159
|
const reconciliation = reconcileCodexGoalSnapshot(
|
|
150
|
-
{ available: true,
|
|
151
|
-
{ expectedObjective: "X"
|
|
160
|
+
{ available: true, status: "active", raw: { goal: { status: "active" } } },
|
|
161
|
+
{ expectedObjective: "X" },
|
|
152
162
|
);
|
|
153
163
|
|
|
154
164
|
// then
|
|
155
|
-
expect(reconciliation.ok).toBe(
|
|
156
|
-
expect(reconciliation.errors
|
|
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"
|
|
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
|
|
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
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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(
|
|
70
|
-
|
|
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
|
-
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
],
|