open-multi-agent-kit 0.91.0 → 0.93.0

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 (168) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/README.md +2 -2
  3. package/dist/core/agent-session.d.ts +29 -0
  4. package/dist/core/agent-session.d.ts.map +1 -1
  5. package/dist/core/agent-session.js +89 -21
  6. package/dist/core/agent-session.js.map +1 -1
  7. package/dist/core/command-safety.d.ts.map +1 -1
  8. package/dist/core/command-safety.js +0 -50
  9. package/dist/core/command-safety.js.map +1 -1
  10. package/dist/core/compaction/branch-summarization.d.ts +5 -1
  11. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  12. package/dist/core/compaction/branch-summarization.js +5 -7
  13. package/dist/core/compaction/branch-summarization.js.map +1 -1
  14. package/dist/core/compaction/compaction.d.ts +12 -3
  15. package/dist/core/compaction/compaction.d.ts.map +1 -1
  16. package/dist/core/compaction/compaction.js +28 -15
  17. package/dist/core/compaction/compaction.js.map +1 -1
  18. package/dist/core/compaction/transaction.d.ts.map +1 -1
  19. package/dist/core/compaction/transaction.js +7 -3
  20. package/dist/core/compaction/transaction.js.map +1 -1
  21. package/dist/core/context-budget-headroom-candidates.d.ts.map +1 -1
  22. package/dist/core/context-budget-headroom-candidates.js +21 -11
  23. package/dist/core/context-budget-headroom-candidates.js.map +1 -1
  24. package/dist/core/context-budget-system-prompt-items.d.ts +1 -0
  25. package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
  26. package/dist/core/context-budget-system-prompt-items.js +7 -8
  27. package/dist/core/context-budget-system-prompt-items.js.map +1 -1
  28. package/dist/core/extensions/builtin/command-safety-gate.d.ts +8 -0
  29. package/dist/core/extensions/builtin/command-safety-gate.d.ts.map +1 -1
  30. package/dist/core/extensions/builtin/command-safety-gate.js +23 -0
  31. package/dist/core/extensions/builtin/command-safety-gate.js.map +1 -1
  32. package/dist/core/extensions/runner.d.ts.map +1 -1
  33. package/dist/core/extensions/runner.js +4 -0
  34. package/dist/core/extensions/runner.js.map +1 -1
  35. package/dist/core/extensions/types.d.ts +2 -0
  36. package/dist/core/extensions/types.d.ts.map +1 -1
  37. package/dist/core/extensions/types.js.map +1 -1
  38. package/dist/core/keybindings.d.ts +5 -0
  39. package/dist/core/keybindings.d.ts.map +1 -1
  40. package/dist/core/keybindings.js +5 -0
  41. package/dist/core/keybindings.js.map +1 -1
  42. package/dist/core/loadouts.d.ts.map +1 -1
  43. package/dist/core/loadouts.js +27 -48
  44. package/dist/core/loadouts.js.map +1 -1
  45. package/dist/core/model-resolver.d.ts +41 -3
  46. package/dist/core/model-resolver.d.ts.map +1 -1
  47. package/dist/core/model-resolver.js +17 -9
  48. package/dist/core/model-resolver.js.map +1 -1
  49. package/dist/core/provider-resilience.d.ts.map +1 -1
  50. package/dist/core/provider-resilience.js +3 -1
  51. package/dist/core/provider-resilience.js.map +1 -1
  52. package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
  53. package/dist/core/reasoning-router-v4-weights.js +9 -3
  54. package/dist/core/reasoning-router-v4-weights.js.map +1 -1
  55. package/dist/core/reasoning-router-v4.d.ts.map +1 -1
  56. package/dist/core/reasoning-router-v4.js +94 -24
  57. package/dist/core/reasoning-router-v4.js.map +1 -1
  58. package/dist/core/resource-loader.d.ts.map +1 -1
  59. package/dist/core/resource-loader.js +11 -5
  60. package/dist/core/resource-loader.js.map +1 -1
  61. package/dist/core/session-file-compaction.d.ts.map +1 -1
  62. package/dist/core/session-file-compaction.js +39 -5
  63. package/dist/core/session-file-compaction.js.map +1 -1
  64. package/dist/core/session-termination.d.ts.map +1 -1
  65. package/dist/core/session-termination.js +1 -1
  66. package/dist/core/session-termination.js.map +1 -1
  67. package/dist/core/settings-manager.d.ts +3 -0
  68. package/dist/core/settings-manager.d.ts.map +1 -1
  69. package/dist/core/settings-manager.js +8 -0
  70. package/dist/core/settings-manager.js.map +1 -1
  71. package/dist/core/system-prompt.d.ts.map +1 -1
  72. package/dist/core/system-prompt.js +36 -31
  73. package/dist/core/system-prompt.js.map +1 -1
  74. package/dist/core/tools/bash.d.ts +2 -0
  75. package/dist/core/tools/bash.d.ts.map +1 -1
  76. package/dist/core/tools/bash.js +38 -11
  77. package/dist/core/tools/bash.js.map +1 -1
  78. package/dist/main.d.ts.map +1 -1
  79. package/dist/main.js +112 -11
  80. package/dist/main.js.map +1 -1
  81. package/dist/modes/interactive/components/control-panel-box.d.ts +27 -0
  82. package/dist/modes/interactive/components/control-panel-box.d.ts.map +1 -1
  83. package/dist/modes/interactive/components/control-panel-box.js +60 -8
  84. package/dist/modes/interactive/components/control-panel-box.js.map +1 -1
  85. package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
  86. package/dist/modes/interactive/components/control-panel-layout.js +100 -45
  87. package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
  88. package/dist/modes/interactive/components/control-panel-runtime-status.d.ts +9 -0
  89. package/dist/modes/interactive/components/control-panel-runtime-status.d.ts.map +1 -1
  90. package/dist/modes/interactive/components/control-panel-runtime-status.js +10 -8
  91. package/dist/modes/interactive/components/control-panel-runtime-status.js.map +1 -1
  92. package/dist/modes/interactive/components/control-panel-sparkles.d.ts +5 -0
  93. package/dist/modes/interactive/components/control-panel-sparkles.d.ts.map +1 -1
  94. package/dist/modes/interactive/components/control-panel-sparkles.js +14 -9
  95. package/dist/modes/interactive/components/control-panel-sparkles.js.map +1 -1
  96. package/dist/modes/interactive/components/footer.d.ts +10 -0
  97. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  98. package/dist/modes/interactive/components/footer.js +3 -3
  99. package/dist/modes/interactive/components/footer.js.map +1 -1
  100. package/dist/modes/interactive/components/status-sidebar.d.ts +49 -0
  101. package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -0
  102. package/dist/modes/interactive/components/status-sidebar.js +253 -0
  103. package/dist/modes/interactive/components/status-sidebar.js.map +1 -0
  104. package/dist/modes/interactive/interactive-mode.d.ts +9 -0
  105. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  106. package/dist/modes/interactive/interactive-mode.js +94 -20
  107. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  108. package/dist/modes/interactive/theme/omk-neon-control.json +13 -13
  109. package/dist/modes/print-mode.d.ts.map +1 -1
  110. package/dist/modes/print-mode.js +9 -0
  111. package/dist/modes/print-mode.js.map +1 -1
  112. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  113. package/dist/modes/rpc/rpc-mode.js +1 -0
  114. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  115. package/dist/utils/clipboard.d.ts.map +1 -1
  116. package/dist/utils/clipboard.js +19 -8
  117. package/dist/utils/clipboard.js.map +1 -1
  118. package/docs/adaptorch-preview-spec.md +174 -0
  119. package/docs/adaptorch-preview.md +1 -1
  120. package/docs/compaction.md +20 -13
  121. package/docs/correctness-wall.md +13 -5
  122. package/docs/custom-provider.md +8 -8
  123. package/docs/development.md +2 -2
  124. package/docs/docs.json +4 -0
  125. package/docs/environment-variables.md +90 -0
  126. package/docs/extensions.md +8 -8
  127. package/docs/index.md +1 -0
  128. package/docs/json.md +4 -4
  129. package/docs/providers.md +1 -1
  130. package/docs/rpc.md +5 -2
  131. package/docs/session-format.md +5 -5
  132. package/docs/settings.md +1 -0
  133. package/docs/skills.md +2 -0
  134. package/docs/themes.md +2 -2
  135. package/docs/tui.md +4 -4
  136. package/docs/usage.md +4 -0
  137. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  138. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  139. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  140. package/examples/extensions/gondolin/package-lock.json +2 -2
  141. package/examples/extensions/gondolin/package.json +1 -1
  142. package/examples/extensions/sandbox/package-lock.json +2 -2
  143. package/examples/extensions/sandbox/package.json +1 -1
  144. package/examples/extensions/subagent/README.md +35 -10
  145. package/examples/extensions/subagent/adaptive-agent-runtime.test.ts +234 -0
  146. package/examples/extensions/subagent/adaptive-agent-runtime.ts +240 -0
  147. package/examples/extensions/subagent/adaptive-result.ts +112 -0
  148. package/examples/extensions/subagent/agents.test.ts +86 -0
  149. package/examples/extensions/subagent/agents.ts +18 -4
  150. package/examples/extensions/subagent/capabilities.ts +15 -1
  151. package/examples/extensions/subagent/checkpoint-runtime.test.ts +125 -0
  152. package/examples/extensions/subagent/checkpoint-runtime.ts +198 -0
  153. package/examples/extensions/subagent/deadline-budget.test.ts +135 -0
  154. package/examples/extensions/subagent/deadline-budget.ts +242 -0
  155. package/examples/extensions/subagent/deadline-profile-store.test.ts +72 -0
  156. package/examples/extensions/subagent/deadline-profile-store.ts +172 -0
  157. package/examples/extensions/subagent/domain-profiles.ts +272 -0
  158. package/examples/extensions/subagent/fixtures/fake-omk-json.mjs +13 -0
  159. package/examples/extensions/subagent/fixtures/hanging-process-tree.mjs +40 -0
  160. package/examples/extensions/subagent/index.ts +253 -137
  161. package/examples/extensions/subagent/managed-process.test.ts +178 -0
  162. package/examples/extensions/subagent/managed-process.ts +236 -0
  163. package/examples/extensions/subagent/subagent-extension-smoke.test.ts +112 -0
  164. package/examples/extensions/subagent/subagent-runtime-types.ts +76 -0
  165. package/examples/extensions/with-deps/package-lock.json +2 -2
  166. package/examples/extensions/with-deps/package.json +1 -1
  167. package/npm-shrinkwrap.json +21 -21
  168. package/package.json +10 -7
@@ -0,0 +1,240 @@
1
+ import * as os from "node:os";
2
+ import * as path from "node:path";
3
+ import {
4
+ attemptOutcome,
5
+ createBaseResult,
6
+ finalizeResult,
7
+ mergeForUpdate,
8
+ mergeResult,
9
+ parseProviderModel,
10
+ resultOutput,
11
+ } from "./adaptive-result.ts";
12
+ import {
13
+ buildCheckpointTask,
14
+ checkpointMadeProgress,
15
+ createCheckpointWorkspace,
16
+ createEmptyCheckpoint,
17
+ readCheckpoint,
18
+ removeCheckpointWorkspace,
19
+ writeCheckpoint,
20
+ } from "./checkpoint-runtime.ts";
21
+ import {
22
+ allocateAttemptBudget,
23
+ type DeadlineProfile,
24
+ type ExecutionBudget,
25
+ estimateTaskDemand,
26
+ planTaskShards,
27
+ recommendAttemptCutoff,
28
+ remainingExecutionMs,
29
+ } from "./deadline-budget.ts";
30
+ import type { DeadlineProfileStore } from "./deadline-profile-store.ts";
31
+ import type {
32
+ DeadlineAttemptMetadata,
33
+ DeadlineOutcome,
34
+ SingleResult,
35
+ SubagentAttemptResult,
36
+ } from "./subagent-runtime-types.ts";
37
+
38
+ export interface RunAttemptInput {
39
+ readonly logicalTask: string;
40
+ readonly task: string;
41
+ readonly cutoffMs: number;
42
+ readonly attempt: number;
43
+ readonly shardId: string;
44
+ readonly shardIndex: number;
45
+ readonly shardCount: number;
46
+ readonly checkpointFilePath: string;
47
+ readonly onUpdate?: (partial: SingleResult) => void;
48
+ }
49
+
50
+ interface AdaptiveRuntimePolicy {
51
+ readonly fallbackAttemptMs: number;
52
+ readonly minimumAttemptMs: number;
53
+ readonly maxTokensPerShard: number;
54
+ }
55
+
56
+ interface RunAdaptiveAgentOptions {
57
+ readonly agentName: string;
58
+ readonly agentSource: "user" | "project";
59
+ readonly logicalTask: string;
60
+ readonly model?: string;
61
+ readonly step?: number;
62
+ readonly signal?: AbortSignal;
63
+ readonly budget: ExecutionBudget;
64
+ readonly pendingSequentialTasks: number;
65
+ readonly checkpointPrefix?: string;
66
+ readonly profileStore: DeadlineProfileStore;
67
+ readonly policy: AdaptiveRuntimePolicy;
68
+ readonly onUpdate?: (partial: SingleResult) => void;
69
+ readonly runAttempt: (input: RunAttemptInput) => Promise<SubagentAttemptResult>;
70
+ }
71
+
72
+ export async function runAdaptiveAgent(options: RunAdaptiveAgentOptions): Promise<SingleResult> {
73
+ const startedAtMs = Date.now();
74
+ let identity = parseProviderModel(options.model);
75
+ let profile = await options.profileStore.profileFor(identity.provider, identity.model);
76
+ const initialCutoff = cutoffFor(profile, options, remainingExecutionMs(options.budget));
77
+ const demand = estimateTaskDemand(options.logicalTask);
78
+ const shards = planTaskShards(options.logicalTask, demand, {
79
+ attemptCutoffMs: initialCutoff,
80
+ maxTaskShards: options.budget.maxTaskShards,
81
+ maxTokensPerShard: options.policy.maxTokensPerShard,
82
+ });
83
+ const taskAllocation = allocateAttemptBudget({
84
+ budget: options.budget,
85
+ pendingSequentialTasks: options.pendingSequentialTasks,
86
+ pendingShards: 1,
87
+ recommendedCutoffMs: remainingExecutionMs(options.budget),
88
+ minimumAttemptMs: 1,
89
+ });
90
+ const taskBudget: ExecutionBudget = {
91
+ ...options.budget,
92
+ hardDeadlineMs: Math.max(1, taskAllocation.taskDeadlineAtMs - startedAtMs),
93
+ deadlineAtMs: Math.min(options.budget.deadlineAtMs, taskAllocation.taskDeadlineAtMs),
94
+ };
95
+ const aggregate = createBaseResult(options);
96
+ const attempts: DeadlineAttemptMetadata[] = [];
97
+ const completedShardIds: string[] = [];
98
+ let resumeCount = 0;
99
+ let duplicateResumeBlocked = false;
100
+ let lastCheckpoint: Awaited<ReturnType<typeof readCheckpoint>> | undefined;
101
+ let terminalOutcome: DeadlineOutcome = "budget-exhausted";
102
+ const checkpointPrefix = options.checkpointPrefix ?? path.join(os.tmpdir(), "omk-subagent-checkpoint-");
103
+ const workspace = await createCheckpointWorkspace(checkpointPrefix);
104
+
105
+ try {
106
+ outer: for (const shard of shards) {
107
+ let shardAttempt = 0;
108
+ let shardMessageCount = 0;
109
+ let checkpoint: typeof lastCheckpoint;
110
+ while (true) {
111
+ const pendingShards = shards.length - completedShardIds.length;
112
+ const recommended = cutoffFor(profile, options, remainingExecutionMs(taskBudget));
113
+ const allocation = allocateAttemptBudget({
114
+ budget: taskBudget,
115
+ pendingSequentialTasks: 1,
116
+ pendingShards,
117
+ recommendedCutoffMs: recommended,
118
+ minimumAttemptMs: options.policy.minimumAttemptMs,
119
+ });
120
+ if (allocation.cutoffMs === 0 || options.signal?.aborted) {
121
+ terminalOutcome = options.signal?.aborted ? "aborted" : "budget-exhausted";
122
+ break outer;
123
+ }
124
+
125
+ const attemptNumber = shardAttempt + 1;
126
+ const task = buildCheckpointTask({
127
+ originalTask: options.logicalTask,
128
+ shardTask: shard.task,
129
+ shardId: shard.id,
130
+ shardIndex: shard.index,
131
+ shardCount: shards.length,
132
+ attempt: attemptNumber,
133
+ cutoffMs: allocation.cutoffMs,
134
+ checkpointFilePath: workspace.filePath,
135
+ ...(checkpoint === undefined ? {} : { checkpoint }),
136
+ });
137
+ const attempt = await options.runAttempt({
138
+ logicalTask: options.logicalTask,
139
+ task,
140
+ cutoffMs: allocation.cutoffMs,
141
+ attempt: attemptNumber,
142
+ shardId: shard.id,
143
+ shardIndex: shard.index,
144
+ shardCount: shards.length,
145
+ checkpointFilePath: workspace.filePath,
146
+ onUpdate: options.onUpdate
147
+ ? (partial) => options.onUpdate?.(mergeForUpdate(aggregate, partial))
148
+ : undefined,
149
+ });
150
+ mergeResult(aggregate, attempt.result);
151
+ shardMessageCount += attempt.result.messages.length;
152
+ const outcome = attemptOutcome(attempt);
153
+ const observedIdentity = parseProviderModel(attempt.result.model ?? options.model);
154
+ if (observedIdentity.provider !== identity.provider || observedIdentity.model !== identity.model) {
155
+ identity = observedIdentity;
156
+ profile = await options.profileStore.profileFor(identity.provider, identity.model);
157
+ }
158
+ try {
159
+ profile = await options.profileStore.record({
160
+ provider: identity.provider,
161
+ model: identity.model,
162
+ outcome,
163
+ elapsedMs: attempt.process.elapsedMs,
164
+ estimatedTokens: demand.estimatedTokens,
165
+ workUnits: demand.workUnits,
166
+ });
167
+ } catch {
168
+ profile = await options.profileStore.profileFor(identity.provider, identity.model);
169
+ }
170
+ lastCheckpoint = await readCheckpoint(workspace.filePath, {
171
+ messageCount: shardMessageCount,
172
+ outputTail: resultOutput(attempt.result),
173
+ lastEventAtMs: Date.now(),
174
+ });
175
+ attempts.push({
176
+ attempt: attemptNumber,
177
+ shardId: shard.id,
178
+ cutoffMs: allocation.cutoffMs,
179
+ elapsedMs: attempt.process.elapsedMs,
180
+ outcome,
181
+ exitCode: attempt.result.exitCode,
182
+ messageCount: attempt.result.messages.length,
183
+ processReason: attempt.process.reason,
184
+ cleanup: attempt.process.cleanup,
185
+ });
186
+
187
+ if (outcome === "completed") {
188
+ completedShardIds.push(shard.id);
189
+ terminalOutcome = "completed";
190
+ checkpoint = undefined;
191
+ await writeCheckpoint(workspace.filePath, createEmptyCheckpoint());
192
+ continue outer;
193
+ }
194
+ terminalOutcome = outcome;
195
+ if (outcome !== "cutoff" || resumeCount >= taskBudget.maxResumeAttempts) break outer;
196
+ if (!checkpointMadeProgress(checkpoint, lastCheckpoint)) {
197
+ duplicateResumeBlocked = true;
198
+ break outer;
199
+ }
200
+ checkpoint = lastCheckpoint;
201
+ shardAttempt += 1;
202
+ resumeCount += 1;
203
+ }
204
+ }
205
+ } finally {
206
+ await removeCheckpointWorkspace(workspace);
207
+ }
208
+
209
+ const remainingShardIds = shards.map((shard) => shard.id).filter((id) => !completedShardIds.includes(id));
210
+ if (remainingShardIds.length === 0) terminalOutcome = "completed";
211
+ finalizeResult(aggregate, terminalOutcome);
212
+ aggregate.deadline = {
213
+ outcome: terminalOutcome,
214
+ startedAtMs,
215
+ elapsedMs: Date.now() - startedAtMs,
216
+ hardDeadlineMs: taskBudget.hardDeadlineMs,
217
+ estimatedTokens: demand.estimatedTokens,
218
+ workUnits: demand.workUnits,
219
+ predictedMs: demand.predictedMs,
220
+ plannedShardIds: shards.map((shard) => shard.id),
221
+ completedShardIds,
222
+ remainingShardIds,
223
+ resumeCount,
224
+ duplicateResumeBlocked,
225
+ provider: identity.provider,
226
+ model: identity.model,
227
+ profileSamples: profile?.samples ?? 0,
228
+ attempts,
229
+ ...(lastCheckpoint === undefined ? {} : { checkpoint: lastCheckpoint }),
230
+ };
231
+ return aggregate;
232
+ }
233
+
234
+ function cutoffFor(profile: DeadlineProfile | undefined, options: RunAdaptiveAgentOptions, maximumMs: number): number {
235
+ return recommendAttemptCutoff(profile, {
236
+ fallbackMs: options.policy.fallbackAttemptMs,
237
+ minimumMs: Math.min(options.policy.minimumAttemptMs, maximumMs),
238
+ maximumMs: Math.max(1, maximumMs),
239
+ });
240
+ }
@@ -0,0 +1,112 @@
1
+ import type { Message } from "omk-ai";
2
+ import {
3
+ type DeadlineOutcome,
4
+ emptyUsage,
5
+ type SingleResult,
6
+ type SubagentAttemptResult,
7
+ type UsageStats,
8
+ } from "./subagent-runtime-types.ts";
9
+
10
+ interface BaseResultOptions {
11
+ readonly agentName: string;
12
+ readonly agentSource: "user" | "project";
13
+ readonly logicalTask: string;
14
+ readonly model?: string;
15
+ readonly step?: number;
16
+ }
17
+
18
+ export function createBaseResult(options: BaseResultOptions): SingleResult {
19
+ return {
20
+ agent: options.agentName,
21
+ agentSource: options.agentSource,
22
+ task: options.logicalTask,
23
+ exitCode: 0,
24
+ messages: [],
25
+ stderr: "",
26
+ usage: emptyUsage(),
27
+ model: options.model,
28
+ step: options.step,
29
+ };
30
+ }
31
+
32
+ export function attemptOutcome(attempt: SubagentAttemptResult): Exclude<DeadlineOutcome, "budget-exhausted"> {
33
+ if (attempt.process.reason === "cutoff") return "cutoff";
34
+ if (attempt.process.reason === "aborted") return "aborted";
35
+ return attempt.result.exitCode === 0 && attempt.result.stopReason !== "error" ? "completed" : "failed";
36
+ }
37
+
38
+ export function mergeResult(target: SingleResult, source: SingleResult): void {
39
+ target.messages.push(...source.messages);
40
+ target.stderr = [target.stderr, source.stderr].filter(Boolean).join("\n");
41
+ target.usage = mergeUsage(target.usage, source.usage);
42
+ target.output = mergeOutput(target.output ?? "", resultOutput(source));
43
+ target.exitCode = source.exitCode;
44
+ target.stopReason = source.stopReason;
45
+ target.errorMessage = source.errorMessage;
46
+ target.model = source.model ?? target.model;
47
+ }
48
+
49
+ export function mergeForUpdate(base: SingleResult, partial: SingleResult): SingleResult {
50
+ const merged: SingleResult = { ...base, messages: [...base.messages], usage: { ...base.usage } };
51
+ mergeResult(merged, partial);
52
+ return merged;
53
+ }
54
+
55
+ export function resultOutput(result: SingleResult): string {
56
+ if (result.output !== undefined) return result.output;
57
+ for (let index = result.messages.length - 1; index >= 0; index--) {
58
+ const message: Message = result.messages[index];
59
+ if (message.role !== "assistant") continue;
60
+ for (const part of message.content) if (part.type === "text") return part.text;
61
+ }
62
+ return "";
63
+ }
64
+
65
+ export function finalizeResult(result: SingleResult, outcome: DeadlineOutcome): void {
66
+ if (outcome === "completed") {
67
+ result.exitCode = 0;
68
+ result.stopReason = "end";
69
+ result.errorMessage = undefined;
70
+ return;
71
+ }
72
+ if (outcome === "failed") {
73
+ result.exitCode = Math.max(1, result.exitCode);
74
+ result.stopReason = "error";
75
+ result.errorMessage ??= "Subagent process failed";
76
+ return;
77
+ }
78
+ result.exitCode = outcome === "aborted" ? 130 : 124;
79
+ result.stopReason = outcome === "aborted" ? "aborted" : "deadline";
80
+ result.errorMessage ??=
81
+ outcome === "budget-exhausted" ? "Subagent execution budget exhausted" : "Subagent deadline cutoff";
82
+ }
83
+
84
+ export function parseProviderModel(value: string | undefined): { provider: string; model: string } {
85
+ if (!value) return { provider: "unknown", model: "default" };
86
+ const separator = value.indexOf("/");
87
+ return separator < 1
88
+ ? { provider: "unknown", model: value }
89
+ : { provider: value.slice(0, separator), model: value.slice(separator + 1) || "default" };
90
+ }
91
+
92
+ function mergeUsage(left: UsageStats, right: UsageStats): UsageStats {
93
+ return {
94
+ input: left.input + right.input,
95
+ output: left.output + right.output,
96
+ cacheRead: left.cacheRead + right.cacheRead,
97
+ cacheWrite: left.cacheWrite + right.cacheWrite,
98
+ cost: left.cost + right.cost,
99
+ contextTokens: Math.max(left.contextTokens, right.contextTokens),
100
+ turns: left.turns + right.turns,
101
+ };
102
+ }
103
+
104
+ function mergeOutput(left: string, right: string): string {
105
+ if (right === "" || left === right || left.endsWith(right)) return left;
106
+ if (left === "") return right;
107
+ const maxOverlap = Math.min(left.length, right.length, 4_096);
108
+ for (let overlap = maxOverlap; overlap > 0; overlap--) {
109
+ if (left.endsWith(right.slice(0, overlap))) return `${left}${right.slice(overlap)}`;
110
+ }
111
+ return `${left}\n\n${right}`;
112
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Unit tests for `parseEnforceFlag` coercion in agents discovery.
3
+ *
4
+ * Regression: 2026-07-26 — YAML boolean `enforceCapabilities: true` was
5
+ * silently ignored because the parser only accepted the string "true". These
6
+ * tests pin coercion behavior for boolean/number/string/garbage inputs via
7
+ * real `discoverAgents` against a temp agents dir (no I/O mocks, no LLM).
8
+ */
9
+ import * as fs from "node:fs";
10
+ import * as os from "node:os";
11
+ import * as path from "node:path";
12
+ import { afterEach, describe, expect, it } from "vitest";
13
+ import { discoverAgents } from "./agents.ts";
14
+
15
+ const tmpDirs: string[] = [];
16
+
17
+ function agent(name: string, fm: string): string {
18
+ return `---\nname: ${name}\ndescription: test agent\ntools: read\n${fm}\n---\nbody\n`;
19
+ }
20
+
21
+ // discoverAgents(cwd, scope) resolves the user dir from OMK_CODING_AGENT_DIR.
22
+ // Point it at our temp dir via env so no real agent tree is touched.
23
+ function withAgentDir(dir: string, fn: () => ReturnType<typeof discoverAgents>) {
24
+ const prev = process.env.OMK_CODING_AGENT_DIR;
25
+ process.env.OMK_CODING_AGENT_DIR = path.dirname(dir); // parent contains "agents"
26
+ try {
27
+ return fn();
28
+ } finally {
29
+ if (prev === undefined) delete process.env.OMK_CODING_AGENT_DIR;
30
+ else process.env.OMK_CODING_AGENT_DIR = prev;
31
+ }
32
+ }
33
+
34
+ afterEach(() => {
35
+ for (const dir of tmpDirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
36
+ });
37
+
38
+ describe("parseEnforceFlag coercion (via discoverAgents)", () => {
39
+ const cases: Array<[string, string, boolean | undefined]> = [
40
+ ["yaml-bool-true", "enforceCapabilities: true", true],
41
+ ["yaml-bool-false", "enforceCapabilities: false", false],
42
+ ["num-1", "enforceCapabilities: 1", true],
43
+ ["num-0", "enforceCapabilities: 0", false],
44
+ ["str-yes", "enforceCapabilities: yes", true],
45
+ ["str-on", "enforceCapabilities: on", true],
46
+ ["str-no", "enforceCapabilities: no", false],
47
+ ["str-off", "enforceCapabilities: off", false],
48
+ ["str-quoted-true", 'enforceCapabilities: "true"', true],
49
+ ["str-quoted-false", 'enforceCapabilities: "false"', false],
50
+ ["str-True-spaced", "enforceCapabilities: ' True '", true],
51
+ ["garbage-num-2", "enforceCapabilities: 2", undefined],
52
+ ["garbage-word", "enforceCapabilities: treu", undefined],
53
+ ["garbage-empty", 'enforceCapabilities: ""', undefined],
54
+ ["absent-key", "", undefined],
55
+ ];
56
+
57
+ for (const [id, fm, expected] of cases) {
58
+ it(`${id} → ${String(expected)}`, () => {
59
+ const parent = fs.mkdtempSync(path.join(os.tmpdir(), "omk-agent-dir-"));
60
+ tmpDirs.push(parent);
61
+ const agentsDir = path.join(parent, "agents");
62
+ fs.mkdirSync(agentsDir);
63
+ fs.writeFileSync(path.join(agentsDir, `${id}.md`), agent(id, fm), "utf-8");
64
+
65
+ const result = withAgentDir(agentsDir, () => discoverAgents(process.cwd(), "user"));
66
+ const found = result.agents.find((a) => a.name === id);
67
+ expect(found, `agent ${id} discovered`).toBeDefined();
68
+ expect(found?.enforceCapabilities).toBe(expected);
69
+ });
70
+ }
71
+
72
+ it("fail-closed: garbage input never enables enforcement", () => {
73
+ const parent = fs.mkdtempSync(path.join(os.tmpdir(), "omk-agent-dir-"));
74
+ tmpDirs.push(parent);
75
+ const agentsDir = path.join(parent, "agents");
76
+ fs.mkdirSync(agentsDir);
77
+ for (const bad of ["treu", "2", '" "', "[true]", "~"]) {
78
+ const name = `bad-${bad.replace(/[^a-z0-9]/gi, "")}`;
79
+ fs.writeFileSync(path.join(agentsDir, `${name}.md`), agent(name, `enforceCapabilities: ${bad}`), "utf-8");
80
+ }
81
+ const result = withAgentDir(agentsDir, () => discoverAgents(process.cwd(), "user"));
82
+ for (const a of result.agents) {
83
+ expect(a.enforceCapabilities, `${a.name} must not enable on garbage`).not.toBe(true);
84
+ }
85
+ });
86
+ });
@@ -29,6 +29,23 @@ export interface AgentDiscoveryResult {
29
29
  projectAgentsDir: string | null;
30
30
  }
31
31
 
32
+ /** Coerce frontmatter enforceCapabilities (boolean | string | unknown) to boolean|undefined. */
33
+ function parseEnforceFlag(value: unknown): boolean | undefined {
34
+ if (typeof value === "boolean") return value;
35
+ if (typeof value === "number") {
36
+ if (value === 1) return true;
37
+ if (value === 0) return false;
38
+ return undefined;
39
+ }
40
+ if (typeof value === "string") {
41
+ const v = value.trim().toLowerCase();
42
+ if (v === "true" || v === "1" || v === "yes" || v === "on") return true;
43
+ if (v === "false" || v === "0" || v === "no" || v === "off") return false;
44
+ return undefined;
45
+ }
46
+ return undefined;
47
+ }
48
+
32
49
  function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
33
50
  const agents: AgentConfig[] = [];
34
51
 
@@ -67,10 +84,7 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
67
84
  .filter(Boolean);
68
85
 
69
86
  const capabilities = parseCapabilities(frontmatter);
70
- const enforceCapabilities =
71
- typeof frontmatter.enforceCapabilities === "string"
72
- ? frontmatter.enforceCapabilities.trim().toLowerCase() === "true"
73
- : undefined;
87
+ const enforceCapabilities = parseEnforceFlag(frontmatter.enforceCapabilities);
74
88
 
75
89
  agents.push({
76
90
  name: frontmatter.name,
@@ -69,16 +69,19 @@ export const VALID_HOOKS: readonly string[] = [
69
69
  ];
70
70
 
71
71
  /**
72
- * Compiled-in MCP server set — the 18 configured servers sourced from the MCP
72
+ * Compiled-in MCP server set — live OMK MCP ids (mcp.json + known optional servers) sourced from the MCP
73
73
  * configuration. This list MUST be updated if the runtime MCP set changes.
74
74
  */
75
75
  export const VALID_MCP: readonly string[] = [
76
76
  "adaptorch",
77
+ "adaptorch-mcp",
78
+ "blender-mcp",
77
79
  "chrome-devtools",
78
80
  "context7",
79
81
  "fetch",
80
82
  "filesystem",
81
83
  "firecrawl",
84
+ "ghidra",
82
85
  "github",
83
86
  "lean-ctx",
84
87
  "memory",
@@ -88,6 +91,7 @@ export const VALID_MCP: readonly string[] = [
88
91
  "serena",
89
92
  "supermemory",
90
93
  "understand-anything",
94
+ "unity-mcp",
91
95
  "zai-reader",
92
96
  "zai-vision",
93
97
  "zai-zread",
@@ -180,6 +184,16 @@ function addSkillsFromRoot(root: string, skills: Map<string, string>): void {
180
184
  entries.sort();
181
185
  for (const rel of entries) {
182
186
  if (path.basename(rel) !== SKILL_FILE_NAME) continue;
187
+ // Skip archived/corpus trees — they collide with real skill names (e.g. code-review).
188
+ const relPosix = rel.split(path.sep).join("/");
189
+ if (
190
+ relPosix.includes("system-prompts-leaks/") ||
191
+ relPosix.includes("/corpus/") ||
192
+ relPosix.includes("node_modules/") ||
193
+ relPosix.includes("/.git/")
194
+ ) {
195
+ continue;
196
+ }
183
197
  const abs = path.resolve(root, rel);
184
198
  let stat: fs.Stats;
185
199
  try {
@@ -0,0 +1,125 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import {
6
+ buildCheckpointTask,
7
+ checkpointMadeProgress,
8
+ createCheckpointWorkspace,
9
+ readCheckpoint,
10
+ removeCheckpointWorkspace,
11
+ } from "./checkpoint-runtime.ts";
12
+
13
+ const cleanupPaths: string[] = [];
14
+
15
+ afterEach(async () => {
16
+ await Promise.all(cleanupPaths.splice(0).map((entry) => fs.promises.rm(entry, { force: true, recursive: true })));
17
+ });
18
+
19
+ describe("subagent checkpoint runtime", () => {
20
+ it("loads a bounded, typed checkpoint written by a child", async () => {
21
+ const workspace = await createCheckpointWorkspace(path.join(os.tmpdir(), "omk-checkpoint-test-"));
22
+ cleanupPaths.push(workspace.dir);
23
+ await fs.promises.writeFile(
24
+ workspace.filePath,
25
+ JSON.stringify({
26
+ version: 1,
27
+ completed: ["inspect boundary"],
28
+ remaining: ["add tests"],
29
+ summary: "boundary inspected",
30
+ artifacts: ["src/runtime.ts"],
31
+ }),
32
+ "utf8",
33
+ );
34
+
35
+ const checkpoint = await readCheckpoint(workspace.filePath, {
36
+ messageCount: 3,
37
+ outputTail: "stream tail",
38
+ lastEventAtMs: 123,
39
+ });
40
+
41
+ expect(checkpoint).toMatchObject({
42
+ completed: ["inspect boundary"],
43
+ remaining: ["add tests"],
44
+ summary: "boundary inspected",
45
+ artifacts: ["src/runtime.ts"],
46
+ messageCount: 3,
47
+ outputTail: "stream tail",
48
+ lastEventAtMs: 123,
49
+ });
50
+ });
51
+
52
+ it("rejects a checkpoint path replaced with a symbolic link", async () => {
53
+ const workspace = await createCheckpointWorkspace(path.join(os.tmpdir(), "omk-checkpoint-symlink-"));
54
+ cleanupPaths.push(workspace.dir);
55
+ const targetPath = path.join(workspace.dir, "target.json");
56
+ await fs.promises.writeFile(
57
+ targetPath,
58
+ JSON.stringify({ version: 1, completed: ["untrusted target"], remaining: [], summary: "leak", artifacts: [] }),
59
+ "utf8",
60
+ );
61
+ await fs.promises.rm(workspace.filePath);
62
+ await fs.promises.symlink(targetPath, workspace.filePath);
63
+
64
+ const checkpoint = await readCheckpoint(workspace.filePath, {
65
+ messageCount: 0,
66
+ outputTail: "",
67
+ lastEventAtMs: 0,
68
+ });
69
+
70
+ expect(checkpoint.completed).toEqual([]);
71
+ expect(checkpoint.summary).toBe("");
72
+ });
73
+
74
+ it("builds a bounded resume prompt that forbids replaying completed side effects", () => {
75
+ const hugeTail = `old-output-${"x".repeat(20_000)}`;
76
+ const prompt = buildCheckpointTask({
77
+ originalTask: "Implement the deadline runtime.",
78
+ shardTask: "Add the process cleanup path.",
79
+ shardId: "shard-2",
80
+ shardIndex: 1,
81
+ shardCount: 3,
82
+ attempt: 2,
83
+ cutoffMs: 30_000,
84
+ checkpointFilePath: "/tmp/checkpoint.json",
85
+ checkpoint: {
86
+ version: 1,
87
+ completed: ["sent SIGTERM"],
88
+ remaining: ["verify SIGKILL"],
89
+ summary: "graceful termination implemented",
90
+ artifacts: ["process-runtime.ts"],
91
+ messageCount: 4,
92
+ outputTail: hugeTail,
93
+ lastEventAtMs: 100,
94
+ },
95
+ });
96
+
97
+ expect(prompt).toContain("Do not repeat completed side effects");
98
+ expect(prompt).toContain("sent SIGTERM");
99
+ expect(prompt).toContain("verify SIGKILL");
100
+ expect(prompt).toContain("Only unresolved checkpoint units");
101
+ expect(prompt).not.toContain("Add the process cleanup path.");
102
+ expect(prompt.length).toBeLessThan(12_000);
103
+ });
104
+
105
+ it("blocks duplicate resume when neither checkpoint nor streamed evidence advanced", () => {
106
+ const before = {
107
+ version: 1 as const,
108
+ completed: ["a"],
109
+ remaining: ["b"],
110
+ summary: "same",
111
+ artifacts: [],
112
+ messageCount: 2,
113
+ outputTail: "same tail",
114
+ lastEventAtMs: 10,
115
+ };
116
+ expect(checkpointMadeProgress(before, { ...before })).toBe(false);
117
+ expect(checkpointMadeProgress(before, { ...before, messageCount: 3 })).toBe(true);
118
+ });
119
+
120
+ it("removes checkpoint artifacts after the logical task settles", async () => {
121
+ const workspace = await createCheckpointWorkspace(path.join(os.tmpdir(), "omk-checkpoint-cleanup-"));
122
+ await removeCheckpointWorkspace(workspace);
123
+ expect(fs.existsSync(workspace.dir)).toBe(false);
124
+ });
125
+ });