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
@@ -5,6 +5,7 @@ import {
5
5
  normalizeUlwLoopSessionId,
6
6
  type UlwLoopScope,
7
7
  ulwLoopAttemptEvidenceDir,
8
+ ulwLoopEvidenceRoot,
8
9
  ulwLoopGoalsRelativePath,
9
10
  } from "../paths.js";
10
11
  import { addUlwLoopGoal, createUlwLoopPlan, startNextUlwLoop, summarizeUlwLoopPlan } from "../plan-crud.js";
@@ -86,9 +87,10 @@ function unreachable(value: never): never {
86
87
  throw new UlwLoopError(`Unhandled operation: ${String(value)}`, "ULW_LOOP_OPERATION_UNHANDLED");
87
88
  }
88
89
 
89
- function nextActionsFrom(result: object): readonly string[] {
90
- if (!("nextActions" in result) || !Array.isArray(result.nextActions)) return [];
91
- return result.nextActions.filter((action): action is string => typeof action === "string").slice(0, 8);
90
+ function stringsFrom(result: object, key: "nextActions" | "warnings"): readonly string[] {
91
+ if (!(key in result)) return [];
92
+ const value = Object.entries(result).find(([name]) => name === key)?.[1];
93
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string").slice(0, 8) : [];
92
94
  }
93
95
 
94
96
  function checkpointWithValidatedSnapshot(
@@ -116,8 +118,15 @@ export function createAgentToolkit(context: ToolkitContext, deps: AgentToolkitDe
116
118
  ): Promise<ToolkitResponseFor<Operation>> => {
117
119
  try {
118
120
  const result = await fn();
119
- const nextActions = typeof result === "object" && result !== null ? nextActionsFrom(result) : [];
120
- return await notify(operation, { ok: true, operation, result, nextActions });
121
+ const nextActions = typeof result === "object" && result !== null ? stringsFrom(result, "nextActions") : [];
122
+ const warnings = typeof result === "object" && result !== null ? stringsFrom(result, "warnings") : [];
123
+ return await notify(operation, {
124
+ ok: true,
125
+ operation,
126
+ result,
127
+ nextActions,
128
+ ...(warnings.length === 0 ? {} : { warnings }),
129
+ });
121
130
  } catch (error) {
122
131
  if (error instanceof UlwLoopError && error.code === "ULW_LOOP_PLAN_MISSING" && context.surface === "omo-senpi")
123
132
  return notify(
@@ -177,6 +186,7 @@ export function createAgentToolkit(context: ToolkitContext, deps: AgentToolkitDe
177
186
  plan,
178
187
  summary: summarizeUlwLoopPlan(plan),
179
188
  nextActions: statusNextActions(plan, context.surface),
189
+ evidenceRoot: ulwLoopEvidenceRoot(scope),
180
190
  // Attempt directories are an evidence-layout v2 concept; a v1 plan must not advertise one.
181
191
  ...(active === undefined || plan.evidenceLayoutVersion !== 2
182
192
  ? {}
@@ -190,8 +200,8 @@ export function createAgentToolkit(context: ToolkitContext, deps: AgentToolkitDe
190
200
  ? checkpointTemplate(context.cwd, scope, args.goalId, { surface: context.surface })
191
201
  : checkpointWithValidatedSnapshot(context, scope, args),
192
202
  ),
193
- steer: (args) => invoke("steer", () => steerUlwLoop(context.cwd, args, scope)),
194
- addGoal: (args) => invoke("add-goal", () => addUlwLoopGoal(context.cwd, args, scope)),
203
+ steer: (args) => invoke("steer", () => steerUlwLoop(context.cwd, args, scope, context.surface)),
204
+ addGoal: (args) => invoke("add-goal", () => addUlwLoopGoal(context.cwd, args, scope, context.surface)),
195
205
  criteria: (args) =>
196
206
  invoke("criteria", async () => {
197
207
  const plan = await readUlwLoopPlan(context.cwd, scope);
@@ -15,7 +15,12 @@ export type UlwLoopOperation = (typeof ULW_LOOP_OPERATIONS)[number];
15
15
 
16
16
  export interface ToolkitOperationManifest {
17
17
  readonly name: UlwLoopOperation;
18
+ /** The camelCase method that runs this operation on every SDK surface. */
19
+ readonly method: string;
18
20
  readonly mutating: boolean;
21
+ readonly description: string;
22
+ /** Argument field -> type notation; a trailing `?` marks an optional field. */
23
+ readonly args: Readonly<Record<string, string>>;
19
24
  }
20
25
 
21
26
  export interface ToolkitManifest {
@@ -24,19 +29,128 @@ export interface ToolkitManifest {
24
29
  readonly operations: readonly ToolkitOperationManifest[];
25
30
  }
26
31
 
32
+ const CRITERION_INPUT =
33
+ '{ scenario: string, expectedEvidence: string, userModel?: "happy" | "edge" | "regression" | "adversarial", essential?: boolean }';
34
+ const STEER_KINDS =
35
+ '"add_subgoal" | "split_subgoal" | "reorder_pending" | "revise_pending_wording" | "revise_criterion" | "annotate_ledger" | "mark_blocked_superseded"';
36
+
27
37
  export const ULW_LOOP_MANIFEST = {
28
38
  version: 1,
29
39
  name: "ulw-loop",
30
40
  operations: [
31
- { name: "help", mutating: false },
32
- { name: "create-goals", mutating: true },
33
- { name: "status", mutating: false },
34
- { name: "complete-goals", mutating: true },
35
- { name: "checkpoint", mutating: true },
36
- { name: "steer", mutating: true },
37
- { name: "add-goal", mutating: true },
38
- { name: "criteria", mutating: false },
39
- { name: "record-evidence", mutating: true },
40
- { name: "record-review-blockers", mutating: true },
41
+ {
42
+ name: "help",
43
+ method: "help",
44
+ mutating: false,
45
+ description: "Return this manifest: every operation with its method name, argument fields, and what it does.",
46
+ args: {},
47
+ },
48
+ {
49
+ name: "create-goals",
50
+ method: "createGoals",
51
+ mutating: true,
52
+ description:
53
+ "Create the session plan from a brief; one goal per bullet, each seeded with placeholder criteria until revised.",
54
+ args: {
55
+ brief: "string",
56
+ codexGoalMode: '"aggregate" | "per_story"?',
57
+ force: "boolean?",
58
+ validationBatchesJson: "string?",
59
+ },
60
+ },
61
+ {
62
+ name: "status",
63
+ method: "status",
64
+ mutating: false,
65
+ description:
66
+ "Read the plan, its summary, structured nextActions, the stable evidenceRoot, and the active goal's currentAttemptDir.",
67
+ args: {},
68
+ },
69
+ {
70
+ name: "complete-goals",
71
+ method: "completeGoals",
72
+ mutating: true,
73
+ description:
74
+ "Acquire the next eligible goal or resume the in-progress one; returns { done: true } once the aggregate is complete.",
75
+ args: { retryFailed: "boolean?" },
76
+ },
77
+ {
78
+ name: "checkpoint",
79
+ method: "checkpoint",
80
+ mutating: true,
81
+ description:
82
+ "Close the goal as complete, failed, or blocked with evidence; the final goal also needs the quality gate. printTemplate returns that gate's template instead.",
83
+ args: {
84
+ goalId: "string",
85
+ status: '"complete" | "failed" | "blocked"',
86
+ evidence: "string",
87
+ codexGoalJson: "string?",
88
+ qualityGateJson: "string?",
89
+ printTemplate: "true? (with goalId? only)",
90
+ },
91
+ },
92
+ {
93
+ name: "steer",
94
+ method: "steer",
95
+ mutating: true,
96
+ description:
97
+ "Propose an evidence-backed plan mutation; revise_criterion replaces a criterion's scenario, expectedEvidence, or userModel.",
98
+ args: {
99
+ kind: STEER_KINDS,
100
+ source: '"finding" | "user_prompt_submit" | "cli"',
101
+ evidence: "string",
102
+ rationale: "string",
103
+ goalId: "string?",
104
+ criterionId: "string? (revise_criterion)",
105
+ scenario: "string? (revise_criterion)",
106
+ expectedEvidence: "string? (revise_criterion)",
107
+ userModel: '"happy" | "edge" | "regression" | "adversarial"? (revise_criterion)',
108
+ title: "string? (add_subgoal)",
109
+ objective: "string? (add_subgoal)",
110
+ childGoals: "{ title: string, objective: string }[]? (split_subgoal)",
111
+ revisedTitle: "string? (revise_pending_wording)",
112
+ revisedObjective: "string? (revise_pending_wording)",
113
+ pendingOrder: "string[]? (reorder_pending)",
114
+ blockedReason: "string? (mark_blocked_superseded)",
115
+ idempotencyKey: "string?",
116
+ },
117
+ },
118
+ {
119
+ name: "add-goal",
120
+ method: "addGoal",
121
+ mutating: true,
122
+ description:
123
+ "Append a goal; pass successCriteria to define its criteria in the same call, otherwise placeholders name the revise call.",
124
+ args: { title: "string", objective: "string", successCriteria: `${CRITERION_INPUT}[]?` },
125
+ },
126
+ {
127
+ name: "criteria",
128
+ method: "criteria",
129
+ mutating: false,
130
+ description: "List one goal's success criteria with their status and captured evidence.",
131
+ args: { goalId: "string" },
132
+ },
133
+ {
134
+ name: "record-evidence",
135
+ method: "recordEvidence",
136
+ mutating: true,
137
+ description:
138
+ "Record a criterion's pass, fail, or blocked evidence; artifacts must exist (resolved against the session cwd) and are stored with the criterion and the ledger entry.",
139
+ args: {
140
+ goalId: "string",
141
+ criterionId: "string",
142
+ status: '"pass" | "fail" | "blocked"',
143
+ evidence: "string",
144
+ notes: "string?",
145
+ artifacts: "string[]?",
146
+ },
147
+ },
148
+ {
149
+ name: "record-review-blockers",
150
+ method: "recordReviewBlockers",
151
+ mutating: true,
152
+ description: "Mark the final goal review_blocked and append the blocker goal the reviewer verdict names.",
153
+ args: { goalId: "string", title: "string", objective: "string", evidence: "string", codexGoalJson: "string?" },
154
+ },
41
155
  ] satisfies readonly ToolkitOperationManifest[],
42
156
  } satisfies ToolkitManifest;
@@ -63,12 +63,15 @@ export interface ToolkitFailure<Operation extends string = string> {
63
63
  readonly ok: false;
64
64
  readonly operation: Operation;
65
65
  readonly error: ToolkitError;
66
+ readonly warnings?: readonly string[];
66
67
  }
67
68
 
68
69
  type StatusResult = {
69
70
  readonly plan: UlwLoopPlan;
70
71
  readonly summary: ReturnType<typeof summarizeUlwLoopPlan>;
71
72
  readonly nextActions: readonly string[];
73
+ /** Plan-level evidence directory, stable for the whole run (relative to the session cwd). */
74
+ readonly evidenceRoot: string;
72
75
  readonly currentAttemptDir?: string;
73
76
  };
74
77
 
@@ -1,5 +1,6 @@
1
1
  // biome-ignore-all format: compact extracted steering mutations stay below the pure LOC budget.
2
2
  import { seedDefaultSuccessCriteria } from "./plan-crud.js";
3
+ import type { UlwLoopToolkitSurface } from "./surface.js";
3
4
  import type { UlwLoopItem, UlwLoopPlan, UlwLoopSteeringChildGoal, UlwLoopSteeringProposal, UlwLoopSuccessCriterionUserModel } from "./types.js";
4
5
  import { ULW_LOOP_SUCCESS_CRITERION_USER_MODELS } from "./types.js";
5
6
  import { updateBatchesAfterSupersede } from "./validation-batch.js";
@@ -30,9 +31,9 @@ function nextId(plan: UlwLoopPlan, offset: number): string {
30
31
  return `G${String(max + offset).padStart(3, "0")}`;
31
32
  }
32
33
 
33
- export function makeGoal(plan: UlwLoopPlan, childGoal: UlwLoopSteeringChildGoal, evidence: string, now: string, offset: number): UlwLoopItem {
34
+ export function makeGoal(plan: UlwLoopPlan, childGoal: UlwLoopSteeringChildGoal, evidence: string, now: string, offset: number, surface: UlwLoopToolkitSurface = "lazycodex"): UlwLoopItem {
34
35
  const id = nextId(plan, offset); const digits = /^G(\d+)/u.exec(id)?.[1]; const goalIndex = digits === undefined ? plan.goals.length + offset - 1 : Number(digits) - 1;
35
- return { id, title: childGoal.title, objective: childGoal.objective, status: "pending", successCriteria: seedDefaultSuccessCriteria(goalIndex, childGoal.objective), attempt: 0, createdAt: now, updatedAt: now, evidence };
36
+ return { id, title: childGoal.title, objective: childGoal.objective, status: "pending", successCriteria: seedDefaultSuccessCriteria(goalIndex, childGoal.objective, { goalId: id, surface }), attempt: 0, createdAt: now, updatedAt: now, evidence };
36
37
  }
37
38
 
38
39
  export function reviseWording(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, now: string): void {
@@ -41,9 +42,9 @@ export function reviseWording(plan: UlwLoopPlan, proposal: UlwLoopSteeringPropos
41
42
  target.steeringEvidence = proposal.evidence; target.steeringRationale = proposal.rationale; target.updatedAt = now;
42
43
  }
43
44
 
44
- export function splitOrBlock(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, now: string): void {
45
+ export function splitOrBlock(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, now: string, surface: UlwLoopToolkitSurface = "lazycodex"): void {
45
46
  const target = goal(plan, targets(proposal)[0]); if (target === undefined) return;
46
- const replacements = children(proposal).map((item, index) => makeGoal(plan, item, proposal.evidence, now, index + 1));
47
+ const replacements = children(proposal).map((item, index) => makeGoal(plan, item, proposal.evidence, now, index + 1, surface));
47
48
  target.steeringEvidence = proposal.evidence; target.steeringRationale = proposal.rationale; target.updatedAt = now;
48
49
  if (replacements.length === 0) { target.status = "blocked"; target.steeringStatus = "blocked"; target.blockedReason = proposal.blockedReason ?? proposal.rationale; }
49
50
  else { target.steeringStatus = "superseded"; target.supersededBy = replacements.map((item) => item.id); for (const item of replacements) item.supersedes = [target.id]; plan.goals.splice(plan.goals.indexOf(target) + 1, 0, ...replacements); updateBatchesAfterSupersede(plan, target.id, replacements.map((item) => item.id)); }
@@ -1,4 +1,8 @@
1
- import type { UlwLoopSteeringMutationKind, UlwLoopSteeringSource } from "./constants.js";
1
+ import type {
2
+ UlwLoopSteeringMutationKind,
3
+ UlwLoopSteeringSource,
4
+ UlwLoopSuccessCriterionUserModel,
5
+ } from "./constants.js";
2
6
  import type { UlwLoopItem, UlwLoopPlan } from "./domain-types.js";
3
7
 
4
8
  export interface UlwLoopSteeringInvariantResult {
@@ -25,9 +29,17 @@ export interface UlwLoopSteeringAfterPayload {
25
29
  export interface UlwLoopSteeringProposal {
26
30
  kind: UlwLoopSteeringMutationKind;
27
31
  source: UlwLoopSteeringSource;
32
+ /** Alias of `targetGoalId`; the SDK documentation uses `goalId` like every other operation. */
33
+ goalId?: string;
28
34
  targetGoalId?: string;
29
35
  targetGoalIds?: string[];
30
36
  criterionId?: string;
37
+ /** `revise_criterion` payload: the replacement scenario text. */
38
+ scenario?: string;
39
+ /** `revise_criterion` payload: the replacement observable-proof text. */
40
+ expectedEvidence?: string;
41
+ /** `revise_criterion` payload: the replacement user model. */
42
+ userModel?: UlwLoopSuccessCriterionUserModel;
31
43
  evidence: string;
32
44
  rationale: string;
33
45
  title?: string;
@@ -5,6 +5,7 @@ import { commit } from "./plan-commit.js";
5
5
  import { findAcceptedSteeringLedgerEntry, readUlwLoopPlan, withUlwLoopMutationLock } from "./plan-io.js";
6
6
  import { makeGoal, reviseCriterion, reviseWording, splitOrBlock } from "./steering-mutations.js";
7
7
  import { buildSteeringPlanSnapshot, changedGoalIdsBetween } from "./steering-snapshot.js";
8
+ import type { UlwLoopToolkitSurface } from "./surface.js";
8
9
  import type {
9
10
  SteerUlwLoopResult,
10
11
  UlwLoopItem,
@@ -21,7 +22,7 @@ import { iso, ULW_LOOP_STEERING_MUTATION_KINDS, ULW_LOOP_SUCCESS_CRITERION_USER_
21
22
  import { batchUpdateLedgerEntry } from "./validation-batch.js";
22
23
 
23
24
  const SOURCES = ["user_prompt_submit", "finding", "cli"] as const satisfies readonly UlwLoopSteeringSource[];
24
- const PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
25
+ const PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "acknowledgedDriverObjectives", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
25
26
  const isObject = (value: unknown): value is object => typeof value === "object" && value !== null; const isPlain = (value: unknown): value is object => isObject(value) && !Array.isArray(value);
26
27
  const read = (value: object, key: string): unknown => Object.entries(value).find(([name]) => name === key)?.[1];
27
28
  const isText = (value: unknown): value is string => typeof value === "string" && value.trim().length > 0;
@@ -156,17 +157,17 @@ function validateCriterion(plan: UlwLoopPlan, proposal: object, reasons: string[
156
157
  if (model !== undefined && !isModel(model)) reasons.push("invalid userModel");
157
158
  }
158
159
 
159
- export function applySteeringMutation(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, audit: UlwLoopSteeringAudit): UlwLoopPlan {
160
+ export function applySteeringMutation(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, audit: UlwLoopSteeringAudit, surface: UlwLoopToolkitSurface = "lazycodex"): UlwLoopPlan {
160
161
  const next = structuredClone(plan);
161
162
  if (!audit.invariant.accepted) return next;
162
163
  const now = proposal.now?.toISOString() ?? iso();
163
- if (proposal.kind === "add_subgoal") next.goals.push(makeGoal(next, { title: proposal.title ?? "", objective: proposal.objective ?? "" }, proposal.evidence, now, 1));
164
+ if (proposal.kind === "add_subgoal") next.goals.push(makeGoal(next, { title: proposal.title ?? "", objective: proposal.objective ?? "" }, proposal.evidence, now, 1, surface));
164
165
  if (proposal.kind === "reorder_pending") {
165
166
  const order = pendingOrder(proposal);
166
167
  next.goals = [...order.map((id) => goal(next, id)).filter((item): item is UlwLoopItem => item !== undefined), ...next.goals.filter((item) => !order.includes(item.id))];
167
168
  }
168
169
  if (proposal.kind === "revise_pending_wording") reviseWording(next, proposal, now);
169
- if (proposal.kind === "split_subgoal" || proposal.kind === "mark_blocked_superseded") splitOrBlock(next, proposal, now);
170
+ if (proposal.kind === "split_subgoal" || proposal.kind === "mark_blocked_superseded") splitOrBlock(next, proposal, now, surface);
170
171
  if (proposal.kind === "revise_criterion") reviseCriterion(next, proposal, now);
171
172
  if (proposal.kind !== "annotate_ledger") next.updatedAt = now;
172
173
  return next;
@@ -188,7 +189,7 @@ export function parseUlwLoopSteeringDirective(text: string): UlwLoopSteeringProp
188
189
  }
189
190
  }
190
191
 
191
- export async function steerUlwLoop(repoRoot: string, proposal: UlwLoopSteeringProposal, scope?: UlwLoopScope): Promise<SteerUlwLoopResult> {
192
+ export async function steerUlwLoop(repoRoot: string, proposal: UlwLoopSteeringProposal, scope?: UlwLoopScope, surface: UlwLoopToolkitSurface = "lazycodex"): Promise<SteerUlwLoopResult> {
192
193
  return withUlwLoopMutationLock(repoRoot, scope, async () => {
193
194
  const plan = await readUlwLoopPlan(repoRoot, scope);
194
195
  const key = proposal.idempotencyKey ?? proposal.promptSignature;
@@ -201,7 +202,7 @@ export async function steerUlwLoop(repoRoot: string, proposal: UlwLoopSteeringPr
201
202
  }
202
203
  const audit = validateUlwLoopSteeringProposal(plan, proposal);
203
204
  const accepted = audit.invariant.accepted;
204
- const next = accepted ? applySteeringMutation(plan, proposal, audit) : plan;
205
+ const next = accepted ? applySteeringMutation(plan, proposal, audit, surface) : plan;
205
206
  const finalAudit: UlwLoopSteeringAudit = { ...audit };
206
207
  if (accepted) {
207
208
  const changed = changedGoalIdsBetween(plan, next);
@@ -0,0 +1,56 @@
1
+ import type { UlwLoopSuccessCriterion, UlwLoopSuccessCriterionUserModel } from "./types.js";
2
+ import { ULW_LOOP_SUCCESS_CRITERION_USER_MODELS, UlwLoopError } from "./types.js";
3
+
4
+ export interface SuccessCriterionInput {
5
+ readonly scenario: string;
6
+ readonly expectedEvidence: string;
7
+ readonly userModel?: UlwLoopSuccessCriterionUserModel;
8
+ readonly essential?: boolean;
9
+ }
10
+
11
+ function invalid(message: string, details: Record<string, unknown>): never {
12
+ throw new UlwLoopError(`Invalid successCriteria: ${message}`, "ULW_LOOP_ARGUMENT_INVALID", { details });
13
+ }
14
+
15
+ function isUserModel(value: unknown): value is UlwLoopSuccessCriterionUserModel {
16
+ return typeof value === "string" && ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value);
17
+ }
18
+
19
+ function requireText(value: unknown, field: string, index: number): string {
20
+ const trimmed = typeof value === "string" ? value.trim() : "";
21
+ return trimmed || invalid(`entry ${index + 1} needs a non-empty ${field}.`, { index, field });
22
+ }
23
+
24
+ export function criterionId(index: number): string {
25
+ return `C${String(index + 1).padStart(3, "0")}`;
26
+ }
27
+
28
+ export function criteriaFromInput(input: readonly unknown[]): UlwLoopSuccessCriterion[] {
29
+ if (input.length === 0) invalid("provide at least one criterion or omit the field for placeholders.", { count: 0 });
30
+ return input.map((entry, index) => {
31
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
32
+ return invalid(`entry ${index + 1} must be an object.`, { index });
33
+ const record: Record<string, unknown> = { ...entry };
34
+ const userModel = record["userModel"] ?? "happy";
35
+ if (!isUserModel(userModel))
36
+ return invalid(
37
+ `entry ${index + 1} userModel must be one of ${ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.join(", ")}.`,
38
+ {
39
+ index,
40
+ userModel: String(userModel),
41
+ },
42
+ );
43
+ const essential = record["essential"];
44
+ if (essential !== undefined && typeof essential !== "boolean")
45
+ return invalid(`entry ${index + 1} essential must be a boolean.`, { index });
46
+ return {
47
+ id: criterionId(index),
48
+ scenario: requireText(record["scenario"], "scenario", index),
49
+ userModel,
50
+ expectedEvidence: requireText(record["expectedEvidence"], "expectedEvidence", index),
51
+ essential: essential ?? true,
52
+ capturedEvidence: null,
53
+ status: "pending",
54
+ };
55
+ });
56
+ }
@@ -152,6 +152,41 @@ describe("reconcileCodexGoalSnapshot", () => {
152
152
  expect(reconciliation.ok).toBe(true);
153
153
  expect(reconciliation.errors).toHaveLength(0);
154
154
  expect(reconciliation.warnings.join(" ")).toContain("driver_objective_differs");
155
+ expect(reconciliation.nextActions).toEqual([]);
156
+ expect(reconciliation.unacknowledgedObjective).toBe("X");
157
+ });
158
+
159
+ it("#given the differing objective was acknowledged #when reconciled again #then it is silent", () => {
160
+ const reconciliation = reconcileCodexGoalSnapshot(
161
+ { available: true, objective: " X \n", status: "active", raw: null },
162
+ { expectedObjective: "Y", acknowledgedObjectives: ["X"] },
163
+ );
164
+
165
+ expect(reconciliation.warnings).toEqual([]);
166
+ expect(reconciliation.nextActions).toEqual([]);
167
+ expect(reconciliation.unacknowledgedObjective).toBeUndefined();
168
+ });
169
+
170
+ it("#given no driver snapshot #when reconciled #then create_goal is a next action, not a warning", () => {
171
+ const reconciliation = reconcileCodexGoalSnapshot(null, { expectedObjective: "Y" });
172
+
173
+ expect(reconciliation.nextActions.join(" ")).toContain("create_goal");
174
+ expect(reconciliation.warnings).toEqual([]);
175
+ });
176
+
177
+ it("#given a completed or limited driver #when reconciled #then the advice is a next action", () => {
178
+ const completed = reconcileCodexGoalSnapshot(
179
+ { available: true, objective: "Y", status: "complete", raw: null },
180
+ { expectedObjective: "Y" },
181
+ );
182
+ const limited = reconcileCodexGoalSnapshot(
183
+ { available: true, objective: "Y", status: "usage_limited", raw: null },
184
+ { expectedObjective: "Y" },
185
+ );
186
+
187
+ expect(completed.nextActions.join(" ")).toContain("create_goal");
188
+ expect(limited.nextActions.join(" ")).toContain("/goal resume");
189
+ expect([...completed.warnings, ...limited.warnings]).toEqual([]);
155
190
  });
156
191
 
157
192
  it("accepts a parsable snapshot without objective as advisory", () => {
@@ -176,7 +211,8 @@ describe("reconcileCodexGoalSnapshot", () => {
176
211
  // then
177
212
  expect(reconciliation.ok).toBe(true);
178
213
  expect(reconciliation.errors).toHaveLength(0);
179
- expect(reconciliation.warnings.join(" ")).toContain("/goal resume");
214
+ expect(reconciliation.nextActions.join(" ")).toContain("/goal resume");
215
+ expect(reconciliation.warnings).toEqual([]);
180
216
  });
181
217
  });
182
218
 
@@ -21,12 +21,17 @@ interface Seeded {
21
21
 
22
22
  // One fixture shape for every driver state: a real plan whose first goal is ready to close, so the
23
23
  // only thing under test is what the driver snapshot does to the checkpoint.
24
- async function seed(sessionId: string): Promise<Seeded> {
24
+ async function seed(sessionId: string, brief = "- alpha goal\n- beta goal"): Promise<Seeded> {
25
25
  const cwd = await mkdtemp(join(tmpdir(), "ulw-driver-"));
26
26
  workDirs.push(cwd);
27
27
  const toolkit = createAgentToolkit({ cwd, sessionId, surface: "omo-senpi" });
28
- const created = await toolkit.createGoals({ brief: "- alpha goal\n- beta goal" });
28
+ const created = await toolkit.createGoals({ brief });
29
29
  if (!created.ok) throw new Error("fixture could not create goals");
30
+ return passNextGoal(cwd, sessionId);
31
+ }
32
+
33
+ async function passNextGoal(cwd: string, sessionId: string): Promise<Seeded> {
34
+ const toolkit = createAgentToolkit({ cwd, sessionId, surface: "omo-senpi" });
30
35
  const started = await toolkit.completeGoals({});
31
36
  if (!started.ok || started.operation !== "complete-goals" || "done" in started.result)
32
37
  throw new Error("fixture could not start a goal");
@@ -46,6 +51,10 @@ async function seed(sessionId: string): Promise<Seeded> {
46
51
  return { cwd, sessionId, goalId, objective: plan.codexObjective ?? "" };
47
52
  }
48
53
 
54
+ function warningsOf(result: object): readonly string[] {
55
+ return "warnings" in result && Array.isArray(result.warnings) ? result.warnings : [];
56
+ }
57
+
49
58
  async function closeWith(seeded: Seeded, snapshot?: Record<string, unknown>) {
50
59
  const toolkit = createAgentToolkit({ cwd: seeded.cwd, sessionId: seeded.sessionId, surface: "omo-senpi" });
51
60
  return toolkit.checkpoint({
@@ -108,15 +117,35 @@ describe("SDK driver-goal states", () => {
108
117
  });
109
118
 
110
119
  describe("#given a driver whose objective differs from the plan", () => {
111
- it("#when the goal is checkpointed #then the difference is reported, never a rejection", async () => {
120
+ it("#when the goal is checkpointed #then the difference is a warning, never a rejection and never a next action", async () => {
112
121
  const seeded = await seed("driver-differs");
113
122
 
114
123
  const closed = await closeWith(seeded, { goal: { objective: "a different objective", status: "active" } });
115
124
 
116
125
  expect(closed.ok).toBe(true);
117
126
  if (closed.ok) {
118
- expect([...(closed.warnings ?? []), ...closed.nextActions].join(" ")).toContain("driver_objective_differs");
127
+ expect(warningsOf(closed.result).join(" ")).toContain("driver_objective_differs");
128
+ expect(closed.nextActions.join(" ")).not.toContain("driver_objective_differs");
129
+ expect(closed.nextActions.join(" ")).not.toContain("create_goal");
130
+ }
131
+ });
132
+
133
+ it("#when a later goal is checkpointed under the same driver #then the difference is acknowledged in the plan and not repeated", async () => {
134
+ const first = await seed("driver-differs-once", "- alpha goal\n- beta goal\n- gamma goal");
135
+ const driver = { goal: { objective: " a different\n objective ", status: "active" } };
136
+
137
+ const firstClose = await closeWith(first, driver);
138
+ const second = await passNextGoal(first.cwd, first.sessionId);
139
+ const secondClose = await closeWith(second, driver);
140
+
141
+ expect(firstClose.ok && secondClose.ok).toBe(true);
142
+ if (firstClose.ok && secondClose.ok) {
143
+ expect(warningsOf(firstClose.result).join(" ")).toContain("driver_objective_differs");
144
+ expect(warningsOf(secondClose.result)).toEqual([]);
145
+ expect(secondClose.nextActions).toEqual([]);
119
146
  }
147
+ const plan = await readUlwLoopPlan(first.cwd, { sessionId: first.sessionId });
148
+ expect(plan.acknowledgedDriverObjectives).toEqual(["a different objective"]);
120
149
  });
121
150
  });
122
151