oh-my-opencode 5.0.0-beta.56 → 5.0.0-beta.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/cli/index.js +18 -22
  2. package/dist/cli-node/index.js +18 -22
  3. package/dist/index.js +16 -16
  4. package/dist/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
  5. package/package.json +16 -16
  6. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  7. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  8. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  9. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  10. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  11. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  12. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  13. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  14. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  15. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  16. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  17. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  18. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  19. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  20. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  21. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  22. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  23. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  25. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  26. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  27. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  28. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.d.ts +5 -1
  29. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.js +4 -3
  30. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.d.ts +5 -1
  31. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +2 -2
  32. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.d.ts +1 -1
  33. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-subcommands.js +38 -39
  34. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +163 -36
  35. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.d.ts +2 -0
  36. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/factory.js +125 -0
  37. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.d.ts +46 -0
  38. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/manifest.js +28 -0
  39. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/types.d.ts +127 -0
  40. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk/types.js +1 -0
  41. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk.d.ts +3 -0
  42. package/packages/omo-codex/plugin/components/ulw-loop/dist/sdk.js +3 -0
  43. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  44. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +4 -0
  46. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-template.ts +8 -2
  47. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +6 -2
  48. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-subcommands.ts +50 -48
  49. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/factory.ts +159 -0
  50. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/manifest.ts +42 -0
  51. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk/types.ts +140 -0
  52. package/packages/omo-codex/plugin/components/ulw-loop/src/sdk.ts +3 -0
  53. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-contract.test.ts +137 -0
  54. package/packages/omo-codex/plugin/components/ulw-loop/test/sdk-session-context.test.ts +148 -0
  55. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  56. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  57. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  58. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  59. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  60. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  61. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  62. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  63. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  64. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  65. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  66. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  67. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  68. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  69. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  70. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  71. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  72. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  73. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  74. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  75. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  76. package/packages/omo-codex/plugin/package-lock.json +12 -12
  77. package/packages/omo-codex/plugin/package.json +1 -1
  78. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +4 -0
  79. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
  80. package/packages/shared-skills/skills/ulw-plan/scripts/scaffold-plan.mjs +1 -1
@@ -0,0 +1,159 @@
1
+ import { checkpointUlwLoop } from "../checkpoint.js";
2
+ import { checkpointTemplate } from "../checkpoint-template.js";
3
+ import { recordEvidence } from "../evidence.js";
4
+ import { type UlwLoopScope, ulwLoopAttemptEvidenceDir } from "../paths.js";
5
+ import { addUlwLoopGoal, createUlwLoopPlan, startNextUlwLoop, summarizeUlwLoopPlan } from "../plan-crud.js";
6
+ import { readUlwLoopPlan } from "../plan-io.js";
7
+ import { recordFinalReviewBlockers } from "../review-blockers.js";
8
+ import { UlwLoopError } from "../runtime.js";
9
+ import { statusNextActions } from "../status-next-actions.js";
10
+ import { steerUlwLoop } from "../steering.js";
11
+ import type { UlwLoopOperation } from "./manifest.js";
12
+ import { ULW_LOOP_MANIFEST } from "./manifest.js";
13
+ import type {
14
+ AgentToolkit,
15
+ AgentToolkitDependencies,
16
+ CheckpointArgs,
17
+ ToolkitContext,
18
+ ToolkitDispatchRequest,
19
+ ToolkitDispatchResponse,
20
+ ToolkitError,
21
+ ToolkitFailure,
22
+ ToolkitResponseFor,
23
+ ToolkitResultFor,
24
+ ToolkitUnknownRequest,
25
+ } from "./types.js";
26
+
27
+ function validateContext(context: ToolkitContext): void {
28
+ if (!context.cwd.trim()) throw new UlwLoopError("cwd is required.", "ULW_LOOP_CWD_REQUIRED");
29
+ if (!context.sessionId.trim())
30
+ throw new UlwLoopError("ULW_LOOP_SESSION_ID_REQUIRED: sessionId is required.", "ULW_LOOP_SESSION_ID_REQUIRED");
31
+ if (context.surface !== "omo-senpi" && context.surface !== "lazycodex")
32
+ throw new UlwLoopError("surface must be omo-senpi or lazycodex.", "ULW_LOOP_SURFACE_INVALID");
33
+ }
34
+
35
+ function errorDetails(error: UlwLoopError): ToolkitError {
36
+ const details =
37
+ error.details === undefined
38
+ ? undefined
39
+ : Object.fromEntries(Object.entries(error.details).map(([key, value]) => [key, String(value)]));
40
+ return details === undefined
41
+ ? { code: error.code, message: error.message }
42
+ : { code: error.code, message: error.message, details };
43
+ }
44
+
45
+ function failure<Operation extends string>(operation: Operation, error: UlwLoopError): ToolkitFailure<Operation> {
46
+ return { ok: false, operation, error: errorDetails(error) };
47
+ }
48
+
49
+ function caught<Operation extends string>(operation: Operation, error: Error): ToolkitFailure<Operation> {
50
+ return failure(operation, error instanceof UlwLoopError ? error : new UlwLoopError(error.message, "ULW_LOOP_ERROR"));
51
+ }
52
+
53
+ function isKnownRequest(request: ToolkitDispatchRequest | ToolkitUnknownRequest): request is ToolkitDispatchRequest {
54
+ return ULW_LOOP_MANIFEST.operations.some((operation) => operation.name === request.operation);
55
+ }
56
+
57
+ function unreachable(value: never): never {
58
+ throw new UlwLoopError(`Unhandled operation: ${String(value)}`, "ULW_LOOP_OPERATION_UNHANDLED");
59
+ }
60
+
61
+ function nextActionsFrom(result: object): readonly string[] {
62
+ if (!("nextActions" in result) || !Array.isArray(result.nextActions)) return [];
63
+ return result.nextActions.filter((action): action is string => typeof action === "string").slice(0, 8);
64
+ }
65
+
66
+ export function createAgentToolkit(context: ToolkitContext, deps: AgentToolkitDependencies = {}): AgentToolkit {
67
+ validateContext(context);
68
+ const scope: UlwLoopScope = { sessionId: context.sessionId };
69
+ const notify = async <Operation extends UlwLoopOperation>(
70
+ operation: Operation,
71
+ response: ToolkitResponseFor<Operation>,
72
+ ): Promise<ToolkitResponseFor<Operation>> => {
73
+ if (deps.hooks?.onOperation !== undefined) await deps.hooks.onOperation({ operation, context, response });
74
+ return response;
75
+ };
76
+ const invoke = async <Operation extends UlwLoopOperation>(
77
+ operation: Operation,
78
+ fn: () => Promise<ToolkitResultFor<Operation>>,
79
+ ): Promise<ToolkitResponseFor<Operation>> => {
80
+ try {
81
+ const result = await fn();
82
+ const nextActions = typeof result === "object" && result !== null ? nextActionsFrom(result) : [];
83
+ return await notify(operation, { ok: true, operation, result, nextActions });
84
+ } catch (error) {
85
+ const response = caught(operation, error instanceof Error ? error : new Error("ULW_LOOP_ERROR"));
86
+ return notify(operation, response);
87
+ }
88
+ };
89
+ const toolkit: AgentToolkit = {
90
+ dispatch: async (request): Promise<ToolkitDispatchResponse> => {
91
+ if (!isKnownRequest(request))
92
+ return failure(
93
+ request.operation,
94
+ new UlwLoopError(`Unknown operation: ${request.operation}`, "ULW_LOOP_OPERATION_UNKNOWN"),
95
+ );
96
+ switch (request.operation) {
97
+ case "help":
98
+ return toolkit.help();
99
+ case "create-goals":
100
+ return toolkit.createGoals(request.args);
101
+ case "status":
102
+ return toolkit.status();
103
+ case "complete-goals":
104
+ return toolkit.completeGoals(request.args);
105
+ case "checkpoint":
106
+ return toolkit.checkpoint(request.args);
107
+ case "steer":
108
+ return toolkit.steer(request.args);
109
+ case "add-goal":
110
+ return toolkit.addGoal(request.args);
111
+ case "criteria":
112
+ return toolkit.criteria(request.args);
113
+ case "record-evidence":
114
+ return toolkit.recordEvidence(request.args);
115
+ case "record-review-blockers":
116
+ return toolkit.recordReviewBlockers(request.args);
117
+ default:
118
+ return unreachable(request);
119
+ }
120
+ },
121
+ help: () => invoke("help", async () => ULW_LOOP_MANIFEST),
122
+ createGoals: (args) => invoke("create-goals", () => createUlwLoopPlan(context.cwd, args, scope)),
123
+ status: () =>
124
+ invoke("status", async () => {
125
+ const plan = await readUlwLoopPlan(context.cwd, scope);
126
+ const active = plan.goals.find((goal) => goal.id === plan.activeGoalId);
127
+ return {
128
+ plan,
129
+ summary: summarizeUlwLoopPlan(plan),
130
+ nextActions: statusNextActions(plan),
131
+ // Attempt directories are an evidence-layout v2 concept; a v1 plan must not advertise one.
132
+ ...(active === undefined || plan.evidenceLayoutVersion !== 2
133
+ ? {}
134
+ : { currentAttemptDir: ulwLoopAttemptEvidenceDir(active.id, active.attempt, scope) }),
135
+ };
136
+ }),
137
+ completeGoals: (args = {}) => invoke("complete-goals", () => startNextUlwLoop(context.cwd, args, scope)),
138
+ checkpoint: (args: CheckpointArgs) =>
139
+ invoke("checkpoint", () =>
140
+ args.printTemplate === true
141
+ ? checkpointTemplate(context.cwd, scope, args.goalId, { surface: context.surface })
142
+ : checkpointUlwLoop(context.cwd, args, scope, { surface: context.surface }),
143
+ ),
144
+ steer: (args) => invoke("steer", () => steerUlwLoop(context.cwd, args, scope)),
145
+ addGoal: (args) => invoke("add-goal", () => addUlwLoopGoal(context.cwd, args, scope)),
146
+ criteria: (args) =>
147
+ invoke("criteria", async () => {
148
+ const plan = await readUlwLoopPlan(context.cwd, scope);
149
+ const goal = plan.goals.find((candidate) => candidate.id === args.goalId);
150
+ if (goal === undefined)
151
+ throw new UlwLoopError(`Unknown ulw-loop id: ${args.goalId}.`, "ULW_LOOP_GOAL_NOT_FOUND");
152
+ return { goalId: goal.id, criteria: goal.successCriteria };
153
+ }),
154
+ recordEvidence: (args) => invoke("record-evidence", () => recordEvidence(context.cwd, args, scope)),
155
+ recordReviewBlockers: (args) =>
156
+ invoke("record-review-blockers", () => recordFinalReviewBlockers(context.cwd, args, scope)),
157
+ };
158
+ return toolkit;
159
+ }
@@ -0,0 +1,42 @@
1
+ export const ULW_LOOP_OPERATIONS = [
2
+ "help",
3
+ "create-goals",
4
+ "status",
5
+ "complete-goals",
6
+ "checkpoint",
7
+ "steer",
8
+ "add-goal",
9
+ "criteria",
10
+ "record-evidence",
11
+ "record-review-blockers",
12
+ ] as const;
13
+
14
+ export type UlwLoopOperation = (typeof ULW_LOOP_OPERATIONS)[number];
15
+
16
+ export interface ToolkitOperationManifest {
17
+ readonly name: UlwLoopOperation;
18
+ readonly mutating: boolean;
19
+ }
20
+
21
+ export interface ToolkitManifest {
22
+ readonly version: 1;
23
+ readonly name: "ulw-loop";
24
+ readonly operations: readonly ToolkitOperationManifest[];
25
+ }
26
+
27
+ export const ULW_LOOP_MANIFEST = {
28
+ version: 1,
29
+ name: "ulw-loop",
30
+ 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
+ ] satisfies readonly ToolkitOperationManifest[],
42
+ } satisfies ToolkitManifest;
@@ -0,0 +1,140 @@
1
+ import type { CheckpointUlwLoopArgs, CheckpointUlwLoopResult } from "../checkpoint.js";
2
+ import type { CheckpointTemplate } from "../checkpoint-template.js";
3
+ import type { recordEvidence } from "../evidence.js";
4
+ import type { addUlwLoopGoal, createUlwLoopPlan, startNextUlwLoop, summarizeUlwLoopPlan } from "../plan-crud.js";
5
+ import type { recordFinalReviewBlockers } from "../review-blockers.js";
6
+ import type { steerUlwLoop } from "../steering.js";
7
+ import type { UlwLoopSteeringProposal } from "../steering-types.js";
8
+ import type { UlwLoopToolkitSurface } from "../surface.js";
9
+ import type { UlwLoopPlan } from "../types.js";
10
+ import type { ULW_LOOP_MANIFEST, UlwLoopOperation } from "./manifest.js";
11
+
12
+ export type ToolkitSurface = UlwLoopToolkitSurface;
13
+
14
+ export interface ToolkitContext {
15
+ readonly cwd: string;
16
+ readonly sessionId: string;
17
+ readonly surface: ToolkitSurface;
18
+ }
19
+
20
+ export type CreateGoalsArgs = Parameters<typeof createUlwLoopPlan>[1];
21
+ export type CompleteGoalsArgs = NonNullable<Parameters<typeof startNextUlwLoop>[1]>;
22
+ export type SteerArgs = UlwLoopSteeringProposal;
23
+ export type AddGoalArgs = Parameters<typeof addUlwLoopGoal>[1];
24
+ export type RecordEvidenceArgs = Parameters<typeof recordEvidence>[1];
25
+ export type CriteriaArgs = { readonly goalId: string };
26
+ export type CheckpointMutationArgs = CheckpointUlwLoopArgs & { readonly printTemplate?: false };
27
+ export type CheckpointTemplateArgs = { readonly printTemplate: true; readonly goalId?: string };
28
+ export type CheckpointArgs = CheckpointMutationArgs | CheckpointTemplateArgs;
29
+ export type RecordReviewBlockersArgs = Parameters<typeof recordFinalReviewBlockers>[1];
30
+
31
+ export type ToolkitDispatchRequest =
32
+ | { readonly operation: "help"; readonly args?: Record<never, never> }
33
+ | { readonly operation: "create-goals"; readonly args: CreateGoalsArgs }
34
+ | { readonly operation: "status"; readonly args?: Record<never, never> }
35
+ | { readonly operation: "complete-goals"; readonly args?: CompleteGoalsArgs }
36
+ | { readonly operation: "checkpoint"; readonly args: CheckpointArgs }
37
+ | { readonly operation: "steer"; readonly args: SteerArgs }
38
+ | { readonly operation: "add-goal"; readonly args: AddGoalArgs }
39
+ | { readonly operation: "criteria"; readonly args: CriteriaArgs }
40
+ | { readonly operation: "record-evidence"; readonly args: RecordEvidenceArgs }
41
+ | { readonly operation: "record-review-blockers"; readonly args: RecordReviewBlockersArgs };
42
+
43
+ export interface ToolkitUnknownRequest {
44
+ readonly operation: string;
45
+ readonly args?: Record<never, never>;
46
+ }
47
+
48
+ export interface ToolkitError {
49
+ readonly code: string;
50
+ readonly message: string;
51
+ readonly details?: Readonly<Record<string, string>>;
52
+ }
53
+
54
+ export interface ToolkitSuccess<Operation extends UlwLoopOperation, Result> {
55
+ readonly ok: true;
56
+ readonly operation: Operation;
57
+ readonly result: Result;
58
+ readonly nextActions: readonly string[];
59
+ readonly warnings?: readonly string[];
60
+ }
61
+
62
+ export interface ToolkitFailure<Operation extends string = string> {
63
+ readonly ok: false;
64
+ readonly operation: Operation;
65
+ readonly error: ToolkitError;
66
+ }
67
+
68
+ type StatusResult = {
69
+ readonly plan: UlwLoopPlan;
70
+ readonly summary: ReturnType<typeof summarizeUlwLoopPlan>;
71
+ readonly nextActions: readonly string[];
72
+ readonly currentAttemptDir?: string;
73
+ };
74
+
75
+ type ResultFor<Operation extends UlwLoopOperation> = Operation extends "help"
76
+ ? typeof ULW_LOOP_MANIFEST
77
+ : Operation extends "create-goals"
78
+ ? Awaited<ReturnType<typeof createUlwLoopPlan>>
79
+ : Operation extends "status"
80
+ ? StatusResult
81
+ : Operation extends "complete-goals"
82
+ ? Awaited<ReturnType<typeof startNextUlwLoop>>
83
+ : Operation extends "checkpoint"
84
+ ? CheckpointUlwLoopResult | CheckpointTemplate
85
+ : Operation extends "steer"
86
+ ? Awaited<ReturnType<typeof steerUlwLoop>>
87
+ : Operation extends "add-goal"
88
+ ? Awaited<ReturnType<typeof addUlwLoopGoal>>
89
+ : Operation extends "criteria"
90
+ ? {
91
+ readonly goalId: string;
92
+ readonly criteria: UlwLoopPlan["goals"][number]["successCriteria"];
93
+ }
94
+ : Operation extends "record-evidence"
95
+ ? Awaited<ReturnType<typeof recordEvidence>>
96
+ : Operation extends "record-review-blockers"
97
+ ? Awaited<ReturnType<typeof recordFinalReviewBlockers>>
98
+ : never;
99
+
100
+ export type ToolkitResultFor<Operation extends UlwLoopOperation> = ResultFor<Operation>;
101
+ export type ToolkitResponseFor<Operation extends UlwLoopOperation> =
102
+ | ToolkitSuccess<Operation, ResultFor<Operation>>
103
+ | ToolkitFailure<Operation>;
104
+ export type ToolkitOperationResponse = {
105
+ [Operation in UlwLoopOperation]: ToolkitResponseFor<Operation>;
106
+ }[UlwLoopOperation];
107
+ export type ToolkitDispatchResponse = ToolkitOperationResponse | ToolkitFailure<string>;
108
+ export type ToolkitResponse<Result> = ToolkitSuccess<UlwLoopOperation, Result> | ToolkitFailure<string>;
109
+
110
+ export interface ToolkitOperationHookEvent<Operation extends UlwLoopOperation = UlwLoopOperation> {
111
+ readonly operation: Operation;
112
+ readonly context: ToolkitContext;
113
+ readonly response: ToolkitResponseFor<Operation>;
114
+ }
115
+
116
+ export interface ToolkitHostHooks {
117
+ readonly onOperation?: <Operation extends UlwLoopOperation>(
118
+ event: ToolkitOperationHookEvent<Operation>,
119
+ ) => void | Promise<void>;
120
+ }
121
+
122
+ export interface AgentToolkitDependencies {
123
+ readonly hooks?: ToolkitHostHooks;
124
+ }
125
+
126
+ export interface AgentToolkit {
127
+ readonly dispatch: (request: ToolkitDispatchRequest | ToolkitUnknownRequest) => Promise<ToolkitDispatchResponse>;
128
+ readonly help: () => Promise<ToolkitResponseFor<"help">>;
129
+ readonly createGoals: (args: CreateGoalsArgs) => Promise<ToolkitResponseFor<"create-goals">>;
130
+ readonly status: () => Promise<ToolkitResponseFor<"status">>;
131
+ readonly completeGoals: (args?: CompleteGoalsArgs) => Promise<ToolkitResponseFor<"complete-goals">>;
132
+ readonly checkpoint: (args: CheckpointArgs) => Promise<ToolkitResponseFor<"checkpoint">>;
133
+ readonly steer: (args: SteerArgs) => Promise<ToolkitResponseFor<"steer">>;
134
+ readonly addGoal: (args: AddGoalArgs) => Promise<ToolkitResponseFor<"add-goal">>;
135
+ readonly criteria: (args: CriteriaArgs) => Promise<ToolkitResponseFor<"criteria">>;
136
+ readonly recordEvidence: (args: RecordEvidenceArgs) => Promise<ToolkitResponseFor<"record-evidence">>;
137
+ readonly recordReviewBlockers: (
138
+ args: RecordReviewBlockersArgs,
139
+ ) => Promise<ToolkitResponseFor<"record-review-blockers">>;
140
+ }
@@ -0,0 +1,3 @@
1
+ export { createAgentToolkit } from "./sdk/factory.js";
2
+ export * from "./sdk/manifest.js";
3
+ export * from "./sdk/types.js";
@@ -0,0 +1,137 @@
1
+ import { mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { seedDefaultSuccessCriteria } from "../src/plan-crud.js";
6
+ import { readUlwLoopPlan } from "../src/plan-io.js";
7
+ import {
8
+ createAgentToolkit,
9
+ type ToolkitContext,
10
+ type ToolkitDispatchRequest,
11
+ ULW_LOOP_MANIFEST,
12
+ ULW_LOOP_OPERATIONS,
13
+ } from "../src/sdk.js";
14
+
15
+ describe("agent toolkit SDK contract", () => {
16
+ it("exposes exactly the ten existing operations in its manifest", () => {
17
+ expect(ULW_LOOP_OPERATIONS).toEqual([
18
+ "help",
19
+ "create-goals",
20
+ "status",
21
+ "complete-goals",
22
+ "checkpoint",
23
+ "steer",
24
+ "add-goal",
25
+ "criteria",
26
+ "record-evidence",
27
+ "record-review-blockers",
28
+ ]);
29
+ expect(ULW_LOOP_MANIFEST.operations.map((operation) => operation.name)).toEqual(ULW_LOOP_OPERATIONS);
30
+ });
31
+
32
+ it("fails closed for an absent session identity", () => {
33
+ expect(() => createAgentToolkit({ cwd: ".", sessionId: "", surface: "lazycodex" })).toThrow(
34
+ "ULW_LOOP_SESSION_ID_REQUIRED",
35
+ );
36
+ });
37
+
38
+ it("uses explicit surface roles for SDK checkpoint templates", async () => {
39
+ for (const surface of ["omo-senpi", "lazycodex"] as const) {
40
+ const cwd = await mkdtemp(join(tmpdir(), `ulw-sdk-template-${surface}-`));
41
+ try {
42
+ const toolkit = createAgentToolkit({ cwd, sessionId: `template-${surface}`, surface });
43
+ const created = await toolkit.createGoals({ brief: "Template fixture" });
44
+ expect(created.ok).toBe(true);
45
+ const template = await toolkit.checkpoint({ printTemplate: true });
46
+ expect(template.ok).toBe(true);
47
+ if (!template.ok) return;
48
+ expect(template.operation).toBe("checkpoint");
49
+ expect("qualityGateTemplate" in template.result).toBe(true);
50
+ if (!("qualityGateTemplate" in template.result)) return;
51
+ expect(template.result.qualityGateTemplate["manualQa"]).toMatchObject({ by: "main-session" });
52
+ expect(template.result.qualityGateTemplate["gateReview"]).toMatchObject({
53
+ by: surface === "omo-senpi" ? "category:deep" : "main-session",
54
+ });
55
+ } finally {
56
+ await rm(cwd, { recursive: true, force: true });
57
+ }
58
+ }
59
+ });
60
+
61
+ it("returns the stable unknown-operation error at the public dispatch boundary", async () => {
62
+ const toolkit = createAgentToolkit({ cwd: ".", sessionId: "sdk-contract", surface: "lazycodex" });
63
+ const result = await toolkit.dispatch({ operation: "does-not-exist" });
64
+ expect(result.ok).toBe(false);
65
+ if (!result.ok) expect(result.error.code).toBe("ULW_LOOP_OPERATION_UNKNOWN");
66
+ });
67
+
68
+ it("calls every operation through typed structured envelopes on a session-scoped fixture", async () => {
69
+ const cwd = await mkdtemp(join(tmpdir(), "ulw-sdk-contract-"));
70
+ try {
71
+ const context: ToolkitContext = { cwd, sessionId: "sdk-contract", surface: "lazycodex" };
72
+ const hookOperations: string[] = [];
73
+ const toolkit = createAgentToolkit(context, {
74
+ hooks: {
75
+ onOperation: ({ operation }) => {
76
+ hookOperations.push(operation);
77
+ },
78
+ },
79
+ });
80
+ const request = async (value: ToolkitDispatchRequest) => {
81
+ const response = await toolkit.dispatch(value);
82
+ expect(response.ok).toBe(true);
83
+ return response;
84
+ };
85
+
86
+ await request({ operation: "help", args: {} });
87
+ await request({ operation: "create-goals", args: { brief: "Build the SDK fixture" } });
88
+ await request({ operation: "status", args: {} });
89
+ const started = await request({ operation: "complete-goals", args: {} });
90
+ if (!started.ok || started.operation !== "complete-goals" || "done" in started.result)
91
+ throw new Error("fixture did not start a goal");
92
+ const goalId = started.result.goal.id;
93
+ const criterionId = seedDefaultSuccessCriteria(0, "Build the SDK fixture")[0]?.id;
94
+ if (criterionId === undefined) throw new Error("fixture has no criterion");
95
+ await request({ operation: "criteria", args: { goalId } });
96
+ await request({
97
+ operation: "record-evidence",
98
+ args: { goalId, criterionId, status: "pass", evidence: "fixture proof" },
99
+ });
100
+ await request({
101
+ operation: "steer",
102
+ args: {
103
+ kind: "annotate_ledger",
104
+ source: "cli",
105
+ evidence: "fixture observation",
106
+ rationale: "fixture rationale",
107
+ },
108
+ });
109
+ const plan = await readUlwLoopPlan(cwd, { sessionId: context.sessionId });
110
+ const review = await request({
111
+ operation: "record-review-blockers",
112
+ args: {
113
+ goalId,
114
+ title: "Review follow-up",
115
+ objective: "Exercise review blockers",
116
+ evidence: "fixture review evidence",
117
+ codexGoalJson: JSON.stringify({ goal: { objective: plan.codexObjective, status: "active" } }),
118
+ },
119
+ });
120
+ if (!review.ok || review.operation !== "record-review-blockers")
121
+ throw new Error("fixture did not record review blockers");
122
+ await request({
123
+ operation: "checkpoint",
124
+ args: { goalId: review.result.newGoal.id, status: "failed", evidence: "fixture failure" },
125
+ });
126
+ await request({
127
+ operation: "add-goal",
128
+ args: { title: "Extra fixture goal", objective: "Exercise add-goal" },
129
+ });
130
+ const operationCount = hookOperations.length;
131
+ expect(operationCount).toBe(10);
132
+ expect(new Set(hookOperations)).toEqual(new Set(ULW_LOOP_OPERATIONS));
133
+ } finally {
134
+ await rm(cwd, { recursive: true, force: true });
135
+ }
136
+ });
137
+ });
@@ -0,0 +1,148 @@
1
+ import { type ChildProcess, spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { afterEach, describe, expect, it } from "vitest";
7
+
8
+ import { ulwLoopLedgerPath, ulwLoopStateLockPath } from "../src/paths.js";
9
+ import { createAgentToolkit } from "../src/sdk.js";
10
+
11
+ const children: ChildProcess[] = [];
12
+ const workDirs: string[] = [];
13
+
14
+ afterEach(async () => {
15
+ for (const child of children.splice(0)) {
16
+ if (child.exitCode === null) child.kill("SIGKILL");
17
+ }
18
+ for (const dir of workDirs.splice(0)) await rm(dir, { recursive: true, force: true });
19
+ });
20
+
21
+ const HOLDER_SCRIPT = `
22
+ const fs = require("node:fs");
23
+ const path = require("node:path");
24
+ const [lockPath, holdMs] = process.argv.slice(1);
25
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
26
+ const fd = fs.openSync(lockPath, "wx");
27
+ fs.writeSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), token: "holder-" + process.pid }));
28
+ fs.closeSync(fd);
29
+ process.stdout.write("HELD\\n");
30
+ setTimeout(() => {
31
+ fs.writeFileSync(lockPath + ".released", "");
32
+ fs.unlinkSync(lockPath);
33
+ }, Number(holdMs));
34
+ `;
35
+
36
+ async function makeWorkdir(): Promise<string> {
37
+ const dir = await mkdtemp(join(tmpdir(), "ulw-sdk-session-"));
38
+ workDirs.push(dir);
39
+ return dir;
40
+ }
41
+
42
+ function holdStateLock(lockPath: string, holdMs: number): Promise<{ readonly releasedMarker: string }> {
43
+ return new Promise((resolve, reject) => {
44
+ const child = spawn(process.execPath, ["-e", HOLDER_SCRIPT, lockPath, String(holdMs)], {
45
+ stdio: ["ignore", "pipe", "inherit"],
46
+ });
47
+ children.push(child);
48
+ let output = "";
49
+ child.stdout.setEncoding("utf8");
50
+ child.stdout.on("data", (chunk: string) => {
51
+ output += chunk;
52
+ if (output.includes("HELD")) resolve({ releasedMarker: `${lockPath}.released` });
53
+ });
54
+ child.once("error", reject);
55
+ });
56
+ }
57
+
58
+ async function seedSession(
59
+ cwd: string,
60
+ sessionId: string,
61
+ ): Promise<{ readonly goalId: string; readonly criteria: readonly string[] }> {
62
+ const toolkit = createAgentToolkit({ cwd, sessionId, surface: "lazycodex" });
63
+ const created = await toolkit.createGoals({ brief: "Session context fixture" });
64
+ if (!created.ok) throw new Error("fixture could not create goals");
65
+ const started = await toolkit.completeGoals({});
66
+ if (!started.ok || started.operation !== "complete-goals" || "done" in started.result)
67
+ throw new Error("fixture could not start a goal");
68
+ const goalId = started.result.goal.id;
69
+ const criteria = await toolkit.criteria({ goalId });
70
+ if (!criteria.ok || criteria.operation !== "criteria") throw new Error("fixture has no criteria");
71
+ return { goalId, criteria: criteria.result.criteria.map((criterion) => criterion.id) };
72
+ }
73
+
74
+ describe("agent toolkit session context", () => {
75
+ describe("#given a plan written under session A", () => {
76
+ it("#when session B reads through its own context #then the read fails closed instead of borrowing A", async () => {
77
+ const cwd = await makeWorkdir();
78
+ await seedSession(cwd, "session-a");
79
+
80
+ const foreign = createAgentToolkit({ cwd, sessionId: "session-b", surface: "lazycodex" });
81
+ const status = await foreign.status();
82
+
83
+ expect(status.ok).toBe(false);
84
+ });
85
+ });
86
+
87
+ describe("#given the session state lock held by another process", () => {
88
+ it("#when an SDK mutation runs #then it waits for that release before writing", async () => {
89
+ const cwd = await makeWorkdir();
90
+ const sessionId = "session-lock";
91
+ const seeded = await seedSession(cwd, sessionId);
92
+ const criterionId = seeded.criteria[0];
93
+ if (criterionId === undefined) throw new Error("fixture has no criterion");
94
+ const holder = await holdStateLock(ulwLoopStateLockPath(cwd, { sessionId }), 300);
95
+
96
+ const toolkit = createAgentToolkit({ cwd, sessionId, surface: "lazycodex" });
97
+ const recorded = await toolkit.recordEvidence({
98
+ goalId: seeded.goalId,
99
+ criterionId,
100
+ status: "pass",
101
+ evidence: "waited for the held lock",
102
+ });
103
+
104
+ expect(recorded.ok).toBe(true);
105
+ expect(existsSync(holder.releasedMarker)).toBe(true);
106
+ });
107
+ });
108
+
109
+ describe("#given two SDK instances bound to one session directory", () => {
110
+ it("#when both record evidence concurrently #then the ledger keeps both entries", async () => {
111
+ const cwd = await makeWorkdir();
112
+ const sessionId = "session-concurrent";
113
+ const seeded = await seedSession(cwd, sessionId);
114
+ const first = seeded.criteria[0];
115
+ const second = seeded.criteria[1];
116
+ if (first === undefined || second === undefined) throw new Error("fixture needs two criteria");
117
+
118
+ const results = await Promise.all([
119
+ createAgentToolkit({ cwd, sessionId, surface: "lazycodex" }).recordEvidence({
120
+ goalId: seeded.goalId,
121
+ criterionId: first,
122
+ status: "pass",
123
+ evidence: "first instance",
124
+ }),
125
+ createAgentToolkit({ cwd, sessionId, surface: "lazycodex" }).recordEvidence({
126
+ goalId: seeded.goalId,
127
+ criterionId: second,
128
+ status: "pass",
129
+ evidence: "second instance",
130
+ }),
131
+ ]);
132
+
133
+ expect(results.every((result) => result.ok)).toBe(true);
134
+ const ledger = await readFile(ulwLoopLedgerPath(cwd, { sessionId }), "utf8");
135
+ const recorded = ledger
136
+ .split("\n")
137
+ .filter((line) => line.trim().length > 0)
138
+ .map((line): unknown => JSON.parse(line))
139
+ .filter((entry): entry is { readonly criterionId: string } => {
140
+ if (typeof entry !== "object" || entry === null || !("criterionId" in entry)) return false;
141
+ return typeof entry.criterionId === "string";
142
+ })
143
+ .map((entry) => entry.criterionId);
144
+ expect(recorded).toContain(first);
145
+ expect(recorded).toContain(second);
146
+ });
147
+ });
148
+ });
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/git-bash/dist/cli.js\" hook post-compact",
9
9
  "timeout": 5,
10
- "statusMessage": "(OmO 5.0.0-beta.56) Resetting Git Bash MCP Reminder",
10
+ "statusMessage": "(OmO 5.0.0-beta.60) Resetting Git Bash MCP Reminder",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\git-bash\\dist\\cli.js\" hook post-compact"
12
12
  }
13
13
  ],
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/lsp/dist/cli.js\" hook post-compact",
9
9
  "timeout": 5,
10
- "statusMessage": "(OmO 5.0.0-beta.56) Resetting LSP Diagnostics Cache",
10
+ "statusMessage": "(OmO 5.0.0-beta.60) Resetting LSP Diagnostics Cache",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\lsp\\dist\\cli.js\" hook post-compact"
12
12
  }
13
13
  ],
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/rules/dist/cli.js\" hook post-compact",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.56) Resetting Project Rule Cache",
10
+ "statusMessage": "(OmO 5.0.0-beta.60) Resetting Project Rule Cache",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\rules\\dist\\cli.js\" hook post-compact"
12
12
  }
13
13
  ],
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/comment-checker/dist/cli.js\" hook post-tool-use",
9
9
  "timeout": 30,
10
- "statusMessage": "(OmO 5.0.0-beta.56) Checking Comments",
10
+ "statusMessage": "(OmO 5.0.0-beta.60) Checking Comments",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\comment-checker\\dist\\cli.js\" hook post-tool-use"
12
12
  }
13
13
  ],