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.
- package/dist/cli/index.js +21 -21
- package/dist/cli-node/index.js +21 -21
- package/package.json +1 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +2 -2
- 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 +7 -5
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-codex-validation.js +7 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +364 -135
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.d.ts +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-snapshot.js +20 -12
- package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +3 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/driver-objective-ack.d.ts +4 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/driver-objective-ack.js +18 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence-artifacts.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence-artifacts.js +37 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/evidence.js +8 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.d.ts +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/paths.js +6 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.d.ts +4 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-crud.js +6 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-goal-factory.d.ts +10 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/plan-goal-factory.js +38 -31
- package/packages/omo-codex/plugin/components/ulw-loop/dist/review-blockers.js +8 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +17 -8
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.d.ts +335 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.js +109 -10
- package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/types.d.ts +3 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.d.ts +3 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-mutations.js +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering-types.d.ts +9 -1
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.d.ts +3 -2
- package/packages/omo-codex/plugin/components/ulw-loop/dist/steering.js +6 -6
- package/packages/omo-codex/plugin/components/ulw-loop/dist/success-criteria-input.d.ts +9 -0
- package/packages/omo-codex/plugin/components/ulw-loop/dist/success-criteria-input.js +41 -0
- 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 +15 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +2 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts +26 -14
- package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +3 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/driver-objective-ack.ts +20 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/evidence-artifacts.ts +45 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts +7 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts +7 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts +9 -3
- package/packages/omo-codex/plugin/components/ulw-loop/src/plan-goal-factory.ts +63 -31
- package/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts +8 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +17 -7
- package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/manifest.ts +124 -10
- package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/types.ts +3 -0
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering-mutations.ts +5 -4
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering-types.ts +13 -1
- package/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts +7 -6
- package/packages/omo-codex/plugin/components/ulw-loop/src/success-criteria-input.ts +56 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-snapshot.test.ts +37 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-driver-states.test.ts +33 -4
- package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-dx.test.ts +219 -0
- 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 +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
|
|
90
|
-
if (!(
|
|
91
|
-
|
|
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 ?
|
|
120
|
-
|
|
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
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
{
|
|
39
|
-
|
|
40
|
-
|
|
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 {
|
|
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.
|
|
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
|
|
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
|
|
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(
|
|
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
|
|