lazycodex-ai 5.0.0-beta.63 → 5.0.0-beta.64

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 (104) hide show
  1. package/dist/cli/index.js +21 -21
  2. package/dist/cli-node/index.js +21 -21
  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/dist/cli.js +2 -2
  6. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  7. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  8. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  9. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  10. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  11. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  12. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  13. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  14. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  15. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  16. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  17. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  18. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  19. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  20. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  21. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  22. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  23. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  24. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  25. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  26. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  27. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.d.ts +7 -5
  28. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.js +7 -2
  29. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +2 -0
  30. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +364 -135
  31. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +6 -0
  32. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +20 -12
  33. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +3 -0
  34. package/packages/omo-codex/plugin/components/ulw-loop/dist/driver-objective-ack.d.ts +4 -0
  35. package/packages/omo-codex/plugin/components/ulw-loop/dist/driver-objective-ack.js +18 -0
  36. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence-artifacts.d.ts +1 -0
  37. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence-artifacts.js +37 -0
  38. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.d.ts +1 -0
  39. package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.js +8 -0
  40. package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.d.ts +1 -0
  41. package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.js +6 -0
  42. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.d.ts +4 -1
  43. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +6 -3
  44. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-goal-factory.d.ts +10 -3
  45. package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-goal-factory.js +38 -31
  46. package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +8 -4
  47. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +17 -8
  48. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.d.ts +335 -0
  49. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.js +109 -10
  50. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/types.d.ts +3 -0
  51. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.d.ts +3 -2
  52. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.js +4 -4
  53. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-types.d.ts +9 -1
  54. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.d.ts +3 -2
  55. package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +6 -6
  56. package/packages/omo-codex/plugin/components/ulw-loop/dist/success-criteria-input.d.ts +9 -0
  57. package/packages/omo-codex/plugin/components/ulw-loop/dist/success-criteria-input.js +41 -0
  58. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  59. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  60. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-codex-validation.ts +15 -3
  61. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +2 -0
  62. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +26 -14
  63. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +3 -0
  64. package/packages/omo-codex/plugin/components/ulw-loop/src/driver-objective-ack.ts +20 -0
  65. package/packages/omo-codex/plugin/components/ulw-loop/src/evidence-artifacts.ts +45 -0
  66. package/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts +7 -0
  67. package/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts +7 -0
  68. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +9 -3
  69. package/packages/omo-codex/plugin/components/ulw-loop/src/plan-goal-factory.ts +63 -31
  70. package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +8 -4
  71. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +17 -7
  72. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/manifest.ts +124 -10
  73. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/types.ts +3 -0
  74. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-mutations.ts +5 -4
  75. package/packages/omo-codex/plugin/components/ulw-loop/src/steering-types.ts +13 -1
  76. package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +7 -6
  77. package/packages/omo-codex/plugin/components/ulw-loop/src/success-criteria-input.ts +56 -0
  78. package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-snapshot.test.ts +37 -1
  79. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-driver-states.test.ts +33 -4
  80. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-dx.test.ts +219 -0
  81. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  82. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  83. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  84. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  85. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  86. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  87. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  88. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  89. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  90. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  91. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  92. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  93. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  94. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  95. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  96. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  97. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  98. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  99. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  100. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  101. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  102. package/packages/omo-codex/plugin/package-lock.json +12 -12
  103. package/packages/omo-codex/plugin/package.json +1 -1
  104. package/packages/omo-codex/scripts/install-dist/install-local.mjs +4 -4
@@ -0,0 +1,41 @@
1
+ import { ULW_LOOP_SUCCESS_CRITERION_USER_MODELS, UlwLoopError } from "./types.js";
2
+ function invalid(message, details) {
3
+ throw new UlwLoopError(`Invalid successCriteria: ${message}`, "ULW_LOOP_ARGUMENT_INVALID", { details });
4
+ }
5
+ function isUserModel(value) {
6
+ return typeof value === "string" && ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value);
7
+ }
8
+ function requireText(value, field, index) {
9
+ const trimmed = typeof value === "string" ? value.trim() : "";
10
+ return trimmed || invalid(`entry ${index + 1} needs a non-empty ${field}.`, { index, field });
11
+ }
12
+ export function criterionId(index) {
13
+ return `C${String(index + 1).padStart(3, "0")}`;
14
+ }
15
+ export function criteriaFromInput(input) {
16
+ if (input.length === 0)
17
+ invalid("provide at least one criterion or omit the field for placeholders.", { count: 0 });
18
+ return input.map((entry, index) => {
19
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
20
+ return invalid(`entry ${index + 1} must be an object.`, { index });
21
+ const record = { ...entry };
22
+ const userModel = record["userModel"] ?? "happy";
23
+ if (!isUserModel(userModel))
24
+ return invalid(`entry ${index + 1} userModel must be one of ${ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.join(", ")}.`, {
25
+ index,
26
+ userModel: String(userModel),
27
+ });
28
+ const essential = record["essential"];
29
+ if (essential !== undefined && typeof essential !== "boolean")
30
+ return invalid(`entry ${index + 1} essential must be a boolean.`, { index });
31
+ return {
32
+ id: criterionId(index),
33
+ scenario: requireText(record["scenario"], "scenario", index),
34
+ userModel,
35
+ expectedEvidence: requireText(record["expectedEvidence"], "expectedEvidence", index),
36
+ essential: essential ?? true,
37
+ capturedEvidence: null,
38
+ status: "pending",
39
+ };
40
+ });
41
+ }
@@ -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.63) Checking Ulw-Loop Steering"
10
+ "statusMessage": "(OmO 5.0.0-beta.64) 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.63) Recording Spawn Admission"
23
+ "statusMessage": "(OmO 5.0.0-beta.64) 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.63) Enforcing Unlimited Ulw-Loop Budget"
36
+ "statusMessage": "(OmO 5.0.0-beta.64) 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.63) Guarding Ulw-Loop Spawns"
47
+ "statusMessage": "(OmO 5.0.0-beta.64) 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.63) Checking Ulw-Loop Resume"
59
+ "statusMessage": "(OmO 5.0.0-beta.64) 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.63",
3
+ "version": "5.0.0-beta.64",
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",
@@ -4,11 +4,19 @@ import {
4
4
  readCodexGoalSnapshotInput,
5
5
  reconcileCodexGoalSnapshot,
6
6
  } from "./codex-goal-snapshot.js";
7
+ import { acknowledgedDriverObjectives } from "./driver-objective-ack.js";
7
8
  import { codexGoalMode, compatibleCodexObjectives, expectedCodexObjective } from "./goal-status.js";
8
9
  import type { UlwLoopScope } from "./paths.js";
9
10
  import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
10
11
  import { UlwLoopError } from "./types.js";
11
12
 
13
+ export interface CheckpointCodexGoalValidation {
14
+ readonly raw: unknown;
15
+ readonly nextActions: readonly string[];
16
+ readonly warnings: readonly string[];
17
+ readonly unacknowledgedObjective?: string;
18
+ }
19
+
12
20
  export async function validateCheckpointCodexGoal(input: {
13
21
  readonly repoRoot: string;
14
22
  readonly plan: UlwLoopPlan;
@@ -16,11 +24,12 @@ export async function validateCheckpointCodexGoal(input: {
16
24
  readonly raw: string | undefined;
17
25
  readonly evidence: string;
18
26
  readonly scope?: UlwLoopScope;
19
- }): Promise<{ readonly raw: unknown; readonly nextActions: readonly string[]; readonly warnings: readonly string[] }> {
27
+ }): Promise<CheckpointCodexGoalValidation> {
20
28
  const snapshot = await readCodexGoalSnapshotInput(input.raw, input.repoRoot);
21
29
  const expected = expectedCodexObjective(input.plan, input.goal);
22
30
  const reconciliation = reconcileCodexGoalSnapshot(snapshot, {
23
31
  expectedObjective: expected,
32
+ acknowledgedObjectives: acknowledgedDriverObjectives(input.plan),
24
33
  ...(codexGoalMode(input.plan) === "aggregate"
25
34
  ? { acceptedObjectives: compatibleCodexObjectives(input.plan) }
26
35
  : {}),
@@ -28,8 +37,11 @@ export async function validateCheckpointCodexGoal(input: {
28
37
  if (!reconciliation.ok) throw new CodexGoalSnapshotError(formatCodexGoalReconciliation(reconciliation));
29
38
  return {
30
39
  raw: snapshot?.raw,
31
- nextActions: reconciliation.warnings,
32
- warnings: reconciliation.warnings.filter((warning) => warning.startsWith("driver_objective_differs")),
40
+ nextActions: reconciliation.nextActions,
41
+ warnings: reconciliation.warnings,
42
+ ...(reconciliation.unacknowledgedObjective === undefined
43
+ ? {}
44
+ : { unacknowledgedObjective: reconciliation.unacknowledgedObjective }),
33
45
  };
34
46
  }
35
47
 
@@ -6,6 +6,7 @@ import {
6
6
  combineCheckpointValidationErrors,
7
7
  validateCheckpointCodexGoal,
8
8
  } from "./checkpoint-codex-validation.js";
9
+ import { acknowledgeDriverObjective } from "./driver-objective-ack.js";
9
10
  import { requireAllCriteriaPass, requireAllPlanCriteriaPass, requireEssentialCriteriaPass } from "./evidence.js";
10
11
  import { codexGoalMode, isFinalRunCompletionCandidate } from "./goal-status.js";
11
12
  import type { UlwLoopScope } from "./paths.js";
@@ -190,6 +191,7 @@ export async function checkpointUlwLoop(
190
191
  codexGoal = validation.raw;
191
192
  nextActions = validation.nextActions;
192
193
  warnings = validation.warnings;
194
+ acknowledgeDriverObjective(plan, validation.unacknowledgedObjective);
193
195
  } catch (error) {
194
196
  if (!(error instanceof UlwLoopError)) throw error;
195
197
  codexValidationError = error;
@@ -22,13 +22,19 @@ export interface CodexGoalSnapshot {
22
22
  export interface CodexGoalReconciliation {
23
23
  ok: boolean;
24
24
  snapshot: CodexGoalSnapshot;
25
+ /** Facts to know (a differing driver objective); never something to do. */
25
26
  warnings: string[];
27
+ /** Things to do next (create, re-create, or resume the driver goal). */
28
+ nextActions: string[];
26
29
  errors: string[];
30
+ /** The differing objective this reconciliation reported for the first time, for the caller to acknowledge. */
31
+ unacknowledgedObjective?: string;
27
32
  }
28
33
 
29
34
  export interface ReconcileCodexGoalOptions {
30
35
  expectedObjective: string;
31
36
  readonly acceptedObjectives?: readonly string[];
37
+ readonly acknowledgedObjectives?: readonly string[];
32
38
  }
33
39
 
34
40
  export class CodexGoalSnapshotError extends Error {}
@@ -111,35 +117,41 @@ export function reconcileCodexGoalSnapshot(
111
117
  const effectiveSnapshot = snapshot ?? { available: false, raw: null };
112
118
  const errors: string[] = [];
113
119
  const warnings: string[] = [];
120
+ const nextActions: string[] = [];
114
121
 
115
122
  const expected = options.expectedObjective;
116
- const normalizedExpected = normalizeObjective(expected);
117
123
  if (!effectiveSnapshot.available) {
118
- warnings.push(`call get_goal; if none, create_goal with codexObjective "${expected}" verbatim`);
119
- return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
124
+ nextActions.push(`call get_goal; if none, create_goal with codexObjective "${expected}" verbatim`);
125
+ return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, nextActions, errors };
120
126
  }
121
127
 
122
- const accepted = new Set(
123
- [
124
- normalizedExpected,
125
- ...(options.acceptedObjectives ?? []).map((objective) => normalizeObjective(objective)),
126
- ].filter(Boolean),
127
- );
128
+ const normalized = (objectives: readonly string[]) => new Set(objectives.map(normalizeObjective).filter(Boolean));
129
+ const accepted = normalized([expected, ...(options.acceptedObjectives ?? [])]);
130
+ const acknowledged = normalized(options.acknowledgedObjectives ?? []);
128
131
  const actual = normalizeObjective(effectiveSnapshot.objective ?? "");
129
- if (actual && !accepted.has(normalizeObjective(actual))) {
132
+ let unacknowledgedObjective: string | undefined;
133
+ if (actual && !accepted.has(actual) && !acknowledged.has(actual)) {
130
134
  warnings.push(`driver_objective_differs: expected "${expected}", got "${actual}".`);
135
+ unacknowledgedObjective = actual;
131
136
  }
132
137
 
133
138
  const actualStatus = effectiveSnapshot.status ?? "unknown";
134
139
  if (actualStatus === "paused" || actualStatus === "usage_limited" || actualStatus === "budget_limited") {
135
- warnings.push("/goal resume or raise the budget");
140
+ nextActions.push("/goal resume or raise the budget");
136
141
  }
137
142
  if (actualStatus === "complete")
138
- warnings.push(`driver closed early: call create_goal with codexObjective "${expected}" verbatim`);
139
- return { ok: errors.length === 0, snapshot: effectiveSnapshot, warnings, errors };
143
+ nextActions.push(`driver closed early: call create_goal with codexObjective "${expected}" verbatim`);
144
+ return {
145
+ ok: errors.length === 0,
146
+ snapshot: effectiveSnapshot,
147
+ warnings,
148
+ nextActions,
149
+ errors,
150
+ ...(unacknowledgedObjective === undefined ? {} : { unacknowledgedObjective }),
151
+ };
140
152
  }
141
153
 
142
154
  export function formatCodexGoalReconciliation(reconciliation: CodexGoalReconciliation): string {
143
- const parts = [...reconciliation.errors, ...reconciliation.warnings];
155
+ const parts = [...reconciliation.errors, ...reconciliation.nextActions, ...reconciliation.warnings];
144
156
  return parts.join(" ");
145
157
  }
@@ -19,6 +19,7 @@ export interface UlwLoopSuccessCriterion {
19
19
  status: UlwLoopCriterionStatus;
20
20
  capturedAt?: string;
21
21
  notes?: string;
22
+ artifacts?: string[];
22
23
  }
23
24
 
24
25
  export interface UlwLoopItem {
@@ -75,6 +76,7 @@ export interface UlwLoopPlan {
75
76
  codexGoalMode?: UlwLoopCodexGoalMode;
76
77
  codexObjective?: string;
77
78
  codexObjectiveAliases?: string[];
79
+ acknowledgedDriverObjectives?: string[];
78
80
  aggregateCompletion?: UlwLoopAggregateCompletion;
79
81
  activeGoalId?: string;
80
82
  validationBatches?: readonly UlwLoopValidationBatch[];
@@ -173,6 +175,7 @@ export interface UlwLoopLedgerEntry {
173
175
  codexGoal?: unknown;
174
176
  evidence?: string;
175
177
  capturedEvidence?: string;
178
+ artifacts?: string[];
176
179
  qualityGate?: unknown;
177
180
  steering?: UlwLoopSteeringAudit;
178
181
  before?: unknown;
@@ -0,0 +1,20 @@
1
+ import type { UlwLoopPlan } from "./types.js";
2
+
3
+ export function normalizeDriverObjective(value: string): string {
4
+ return value.replace(/\s+/g, " ").trim();
5
+ }
6
+
7
+ export function acknowledgedDriverObjectives(plan: UlwLoopPlan): readonly string[] {
8
+ return plan.acknowledgedDriverObjectives ?? [];
9
+ }
10
+
11
+ // A differing driver objective is reported once; the plan remembers the exact text so every later
12
+ // checkpoint under the same driver stays quiet, while a driver rewritten to a third objective is
13
+ // reported again.
14
+ export function acknowledgeDriverObjective(plan: UlwLoopPlan, objective: string | undefined): boolean {
15
+ if (objective === undefined) return false;
16
+ const normalized = normalizeDriverObjective(objective);
17
+ if (!normalized || acknowledgedDriverObjectives(plan).includes(normalized)) return false;
18
+ plan.acknowledgedDriverObjectives = [...acknowledgedDriverObjectives(plan), normalized];
19
+ return true;
20
+ }
@@ -0,0 +1,45 @@
1
+ import { existsSync } from "node:fs";
2
+ import { isAbsolute, relative, resolve, sep } from "node:path";
3
+ import { UlwLoopError } from "./types.js";
4
+
5
+ function fail(message: string, code: string, details: Record<string, unknown>): never {
6
+ throw new UlwLoopError(message, code, { details });
7
+ }
8
+
9
+ function stored(repoRoot: string, absolute: string): string {
10
+ const rel = relative(repoRoot, absolute);
11
+ const inside = rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
12
+ return inside ? rel.split(sep).join("/") : absolute;
13
+ }
14
+
15
+ // Artifacts are bound at record time: a cited path that does not exist is a claim without a file,
16
+ // so the whole record is refused before any state changes. Paths inside the session cwd persist
17
+ // repo-relative (portable across machines); paths outside persist absolute.
18
+ export function resolveEvidenceArtifacts(
19
+ repoRoot: string,
20
+ artifacts: readonly unknown[] | undefined,
21
+ ): string[] | undefined {
22
+ if (artifacts === undefined) return undefined;
23
+ const seen = new Set<string>();
24
+ const resolved: string[] = [];
25
+ for (const [index, candidate] of artifacts.entries()) {
26
+ const path = typeof candidate === "string" ? candidate.trim() : "";
27
+ if (!path) fail(`Artifact ${index + 1} must be a non-empty path.`, "ULW_LOOP_ARGUMENT_INVALID", { index });
28
+ const absolute = resolve(repoRoot, path);
29
+ if (!existsSync(absolute))
30
+ fail(
31
+ `Evidence artifact does not exist: ${path} (resolved to ${absolute}).`,
32
+ "ULW_LOOP_EVIDENCE_ARTIFACT_MISSING",
33
+ {
34
+ path,
35
+ resolved: absolute,
36
+ },
37
+ );
38
+ const value = stored(repoRoot, absolute);
39
+ if (!seen.has(value)) {
40
+ seen.add(value);
41
+ resolved.push(value);
42
+ }
43
+ }
44
+ return resolved;
45
+ }
@@ -1,3 +1,4 @@
1
+ import { resolveEvidenceArtifacts } from "./evidence-artifacts.js";
1
2
  import { essentialCriteriaOf, hasAllCriteriaPass, hasEssentialCriteriaPass } from "./goal-status.js";
2
3
  import type { UlwLoopScope } from "./paths.js";
3
4
  import { commit } from "./plan-commit.js";
@@ -12,6 +13,7 @@ type RecordEvidenceArgs = {
12
13
  readonly status: EvidenceStatus;
13
14
  readonly evidence: string;
14
15
  readonly notes?: string;
16
+ readonly artifacts?: readonly string[];
15
17
  };
16
18
 
17
19
  function ulwLoopFail(message: string, code: string, details: Record<string, unknown>): never {
@@ -67,6 +69,7 @@ export async function recordEvidence(
67
69
  const goal = findGoal(plan, args.goalId);
68
70
  const criterion = findCriterion(goal, args.criterionId);
69
71
  const evidence = nonEmptyEvidence(args.evidence);
72
+ const artifacts = resolveEvidenceArtifacts(repoRoot, args.artifacts);
70
73
  const kind = ledgerKind(args.status);
71
74
  const prevStatus = criterion.status;
72
75
  const capturedAt = iso();
@@ -74,6 +77,8 @@ export async function recordEvidence(
74
77
  criterion.capturedEvidence = evidence;
75
78
  criterion.capturedAt = capturedAt;
76
79
  if (args.notes !== undefined) criterion.notes = args.notes;
80
+ if (artifacts !== undefined) criterion.artifacts = artifacts;
81
+ else delete criterion.artifacts;
77
82
  goal.updatedAt = capturedAt;
78
83
  plan.updatedAt = capturedAt;
79
84
  const ledgerEntry: UlwLoopLedgerEntry = {
@@ -84,6 +89,7 @@ export async function recordEvidence(
84
89
  criterionStatus: args.status,
85
90
  evidence,
86
91
  capturedEvidence: evidence,
92
+ ...(artifacts === undefined ? {} : { artifacts }),
87
93
  before: { status: prevStatus },
88
94
  after: { goalId: goal.id, criterionId: criterion.id, status: args.status, evidence, capturedAt, prevStatus },
89
95
  };
@@ -112,6 +118,7 @@ export async function markCriteriaPendingResetForGoal(
112
118
  criterion.capturedEvidence = null;
113
119
  delete criterion.capturedAt;
114
120
  delete criterion.notes;
121
+ delete criterion.artifacts;
115
122
  }
116
123
  goal.updatedAt = now;
117
124
  plan.updatedAt = now;
@@ -85,6 +85,13 @@ export function repoRelative(absolutePath: string, repoRoot: string): string {
85
85
  return absolutePath.split("\\").join("/");
86
86
  }
87
87
 
88
+ // The plan-level evidence root stays fixed for the whole run, unlike the per-goal attempt dir, so
89
+ // long lanes that span goals have one stable place to write artifacts.
90
+ export function ulwLoopEvidenceRoot(scope?: UlwLoopScope): string {
91
+ const sessionId = normalizeUlwLoopSessionId(scope?.sessionId);
92
+ return sessionId === null ? ".omo/evidence" : `.omo/evidence/ulw/${sessionId}`;
93
+ }
94
+
88
95
  // Both the status --json emitter and the checkpoint enforcement resolve the attempt dir through
89
96
  // this function from the scope alone; a second resolution path (env, a literal placeholder)
90
97
  // would let the gate reject its own advertised directory.
@@ -8,6 +8,7 @@ import {
8
8
  import { beforePlanMutation, commit } from "./plan-commit.js";
9
9
  import { appendGoalToPlan, deriveGoalCandidates, makeGoal } from "./plan-goal-factory.js";
10
10
  import { planExists, readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
11
+ import type { SuccessCriterionInput } from "./success-criteria-input.js";
11
12
  import type { UlwLoopToolkitSurface } from "./surface.js";
12
13
  import type {
13
14
  UlwLoopCodexGoalMode,
@@ -20,6 +21,7 @@ import { iso, UlwLoopError } from "./types.js";
20
21
  import { parseValidationBatches } from "./validation-batch.js";
21
22
 
22
23
  export { deriveGoalCandidates, seedDefaultSuccessCriteria } from "./plan-goal-factory.js";
24
+ export type { SuccessCriterionInput } from "./success-criteria-input.js";
23
25
 
24
26
  export type UlwLoopPlanSummary = {
25
27
  readonly total: number;
@@ -81,7 +83,7 @@ export async function createUlwLoopPlan(
81
83
  }
82
84
  const now = iso();
83
85
  const goals = deriveGoalCandidates(args.brief).map((goal, index) =>
84
- makeGoal(goal.title, goal.objective, index, now),
86
+ makeGoal(goal.title, goal.objective, index, now, { surface }),
85
87
  );
86
88
  const plan: UlwLoopPlan = {
87
89
  version: 1,
@@ -128,13 +130,17 @@ function completedPlanExistsError(scope: UlwLoopScope | undefined, surface: UlwL
128
130
 
129
131
  export async function addUlwLoopGoal(
130
132
  repoRoot: string,
131
- args: { title: string; objective: string },
133
+ args: { title: string; objective: string; successCriteria?: readonly SuccessCriterionInput[] },
132
134
  scope?: UlwLoopScope,
135
+ surface: UlwLoopToolkitSurface = "lazycodex",
133
136
  ): Promise<{ plan: UlwLoopPlan; goal: UlwLoopItem }> {
134
137
  return withUlwLoopMutationLock(repoRoot, scope, async () => {
135
138
  const plan = await readUlwLoopPlan(repoRoot, scope);
136
139
  const now = iso();
137
- const goal = appendGoalToPlan(plan, args.title, args.objective, now);
140
+ const goal = appendGoalToPlan(plan, args.title, args.objective, now, {
141
+ surface,
142
+ ...(args.successCriteria === undefined ? {} : { successCriteria: args.successCriteria }),
143
+ });
138
144
  await commit(repoRoot, scope, {
139
145
  plan,
140
146
  entries: [{ at: now, kind: "goal_added", goalId: goal.id, status: goal.status, message: goal.title }],
@@ -1,6 +1,14 @@
1
+ import { criteriaFromInput, criterionId, type SuccessCriterionInput } from "./success-criteria-input.js";
2
+ import type { UlwLoopToolkitSurface } from "./surface.js";
1
3
  import type { UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "./types.js";
2
4
  import { UlwLoopError } from "./types.js";
3
5
 
6
+ export interface GoalSeedOptions {
7
+ readonly goalId?: string;
8
+ readonly surface?: UlwLoopToolkitSurface;
9
+ readonly successCriteria?: readonly SuccessCriterionInput[];
10
+ }
11
+
4
12
  function cleanLine(line: string): string {
5
13
  return line.replace(/^\s*(?:[-*+]\s+|\d+[.)]\s+)/, "").trim();
6
14
  }
@@ -38,40 +46,44 @@ function truncateObjective(objective: string): string {
38
46
  return objective.length > 80 ? `${objective.slice(0, 77).trimEnd()}...` : objective;
39
47
  }
40
48
 
41
- export function seedDefaultSuccessCriteria(goalIndex: number, objective: string): UlwLoopSuccessCriterion[] {
49
+ // The placeholder must name the exact call that replaces it on the surface the plan is driven from,
50
+ // so a goal seeded without criteria costs one documented call per criterion instead of a doc hunt.
51
+ function replaceVia(goalId: string, id: string, surface: UlwLoopToolkitSurface): string {
52
+ return surface === "omo-senpi"
53
+ ? `Replace via agentToolkit.steer({ kind: "revise_criterion", source: "finding", goalId: "${goalId}", criterionId: "${id}", scenario, expectedEvidence, evidence, rationale }) (or pass successCriteria to addGoal)`
54
+ : `Replace via omo-agent-toolkit ulw-loop steer --kind revise_criterion --goal-id ${goalId} --criterion-id ${id} --scenario "<scenario>" --expected-evidence "<proof>" --evidence "<why>" --rationale "<why>"`;
55
+ }
56
+
57
+ export function seedDefaultSuccessCriteria(
58
+ goalIndex: number,
59
+ objective: string,
60
+ options: Pick<GoalSeedOptions, "goalId" | "surface"> = {},
61
+ ): UlwLoopSuccessCriterion[] {
42
62
  const subject = truncateObjective(normalizeObjective(objective) || `Goal ${goalIndex + 1}`);
63
+ const goalId = options.goalId ?? `G${String(goalIndex + 1).padStart(3, "0")}`;
64
+ const surface = options.surface ?? "lazycodex";
43
65
  const rows = [
66
+ ["happy", `happy path for: ${subject}`, `observable happy-path proof for goal ${goalIndex + 1}`, true],
67
+ ["edge", "edge case (boundary/empty/malformed)", `boundary or malformed-input proof for: ${subject}`, true],
44
68
  [
45
- "C001",
46
- "happy",
47
- `happy path for: ${subject}`,
48
- `Replace via revise_criterion with observable happy-path proof for goal ${goalIndex + 1}.`,
49
- true,
50
- ],
51
- [
52
- "C002",
53
- "edge",
54
- "edge case (boundary/empty/malformed)",
55
- `Replace via revise_criterion with boundary or malformed-input proof for: ${subject}.`,
56
- true,
57
- ],
58
- [
59
- "C003",
60
69
  "regression",
61
70
  "regression: adjacent surface still works",
62
- `Replace via revise_criterion with regression proof for neighboring behavior after: ${subject}.`,
71
+ `regression proof for neighboring behavior after: ${subject}`,
63
72
  false,
64
73
  ],
65
74
  ] as const;
66
- return rows.map(([id, userModel, scenario, expectedEvidence, essential]) => ({
67
- id,
68
- scenario,
69
- userModel,
70
- expectedEvidence,
71
- essential,
72
- capturedEvidence: null,
73
- status: "pending",
74
- }));
75
+ return rows.map(([userModel, scenario, proof, essential], index) => {
76
+ const id = criterionId(index);
77
+ return {
78
+ id,
79
+ scenario,
80
+ userModel,
81
+ expectedEvidence: `${replaceVia(goalId, id, surface)} with ${proof}.`,
82
+ essential,
83
+ capturedEvidence: null,
84
+ status: "pending",
85
+ };
86
+ });
75
87
  }
76
88
 
77
89
  export function deriveGoalCandidates(brief: string): Array<{ title: string; objective: string }> {
@@ -101,23 +113,43 @@ export function deriveGoalCandidates(brief: string): Array<{ title: string; obje
101
113
  }));
102
114
  }
103
115
 
104
- export function makeGoal(title: string, objective: string, index: number, now: string): UlwLoopItem {
116
+ export function makeGoal(
117
+ title: string,
118
+ objective: string,
119
+ index: number,
120
+ now: string,
121
+ options: GoalSeedOptions = {},
122
+ ): UlwLoopItem {
105
123
  const cleanTitle = assertNonEmpty(title, "title");
106
124
  const cleanObjective = assertNonEmpty(objective, "objective");
125
+ const id = normalizeGoalId(cleanTitle, index);
126
+ const successCriteria =
127
+ options.successCriteria === undefined
128
+ ? seedDefaultSuccessCriteria(index, cleanObjective, {
129
+ goalId: id,
130
+ ...(options.surface === undefined ? {} : { surface: options.surface }),
131
+ })
132
+ : criteriaFromInput(options.successCriteria);
107
133
  return {
108
- id: normalizeGoalId(cleanTitle, index),
134
+ id,
109
135
  title: cleanTitle,
110
136
  objective: cleanObjective,
111
137
  status: "pending",
112
- successCriteria: seedDefaultSuccessCriteria(index, cleanObjective),
138
+ successCriteria,
113
139
  attempt: 0,
114
140
  createdAt: now,
115
141
  updatedAt: now,
116
142
  };
117
143
  }
118
144
 
119
- export function appendGoalToPlan(plan: UlwLoopPlan, title: string, objective: string, now: string): UlwLoopItem {
120
- const goal = makeGoal(title, objective, plan.goals.length, now);
145
+ export function appendGoalToPlan(
146
+ plan: UlwLoopPlan,
147
+ title: string,
148
+ objective: string,
149
+ now: string,
150
+ options: GoalSeedOptions = {},
151
+ ): UlwLoopItem {
152
+ const goal = makeGoal(title, objective, plan.goals.length, now, options);
121
153
  plan.goals.push(goal);
122
154
  plan.updatedAt = now;
123
155
  return goal;
@@ -6,6 +6,7 @@ import {
6
6
  readCodexGoalSnapshotInput,
7
7
  reconcileCodexGoalSnapshot,
8
8
  } from "./codex-goal-snapshot.js";
9
+ import { acknowledgeDriverObjective, acknowledgedDriverObjectives } from "./driver-objective-ack.js";
9
10
  import { compatibleCodexObjectives, expectedCodexObjective, isFinalRunCompletionCandidate } from "./goal-status.js";
10
11
  import type { UlwLoopScope } from "./paths.js";
11
12
  import { commit } from "./plan-commit.js";
@@ -40,12 +41,13 @@ function nextGoalId(plan: UlwLoopPlan): string {
40
41
 
41
42
  function appendBlockerGoal(plan: UlwLoopPlan, args: RecordFinalReviewBlockersArgs, now: string): UlwLoopItem {
42
43
  const index = plan.goals.length;
44
+ const id = nextGoalId(plan);
43
45
  const goal: UlwLoopItem = {
44
- id: nextGoalId(plan),
46
+ id,
45
47
  title: args.title,
46
48
  objective: args.objective,
47
49
  status: "pending",
48
- successCriteria: seedDefaultSuccessCriteria(index, args.objective),
50
+ successCriteria: seedDefaultSuccessCriteria(index, args.objective, { goalId: id }),
49
51
  attempt: 0,
50
52
  createdAt: now,
51
53
  updatedAt: now,
@@ -70,8 +72,10 @@ export async function recordFinalReviewBlockers(
70
72
  const reconciliation = reconcileCodexGoalSnapshot(snapshot, {
71
73
  expectedObjective: expectedCodexObjective(plan, goal),
72
74
  acceptedObjectives: compatibleCodexObjectives(plan),
75
+ acknowledgedObjectives: acknowledgedDriverObjectives(plan),
73
76
  });
74
77
  if (!reconciliation.ok) throw new CodexGoalSnapshotError(formatCodexGoalReconciliation(reconciliation));
78
+ acknowledgeDriverObjective(plan, reconciliation.unacknowledgedObjective);
75
79
 
76
80
  const now = iso();
77
81
  for (const field of BLOCKER_FIELDS) Reflect.deleteProperty(goal, field);
@@ -95,8 +99,8 @@ export async function recordFinalReviewBlockers(
95
99
  blockedGoal: goal,
96
100
  newGoal,
97
101
  ledgerEntries,
98
- nextActions: reconciliation.warnings,
99
- warnings: reconciliation.warnings.filter((warning) => warning.startsWith("driver_objective_differs")),
102
+ nextActions: reconciliation.nextActions,
103
+ warnings: reconciliation.warnings,
100
104
  };
101
105
  });
102
106
  }