open-multi-agent-kit 0.78.1 → 0.78.3

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 (102) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/MATURITY.md +4 -0
  3. package/README.md +70 -1
  4. package/dist/benchmark/contracts.d.ts +116 -0
  5. package/dist/benchmark/contracts.js +6 -0
  6. package/dist/benchmark/fixtures.d.ts +11 -0
  7. package/dist/benchmark/fixtures.js +121 -0
  8. package/dist/benchmark/harness.d.ts +13 -0
  9. package/dist/benchmark/harness.js +191 -0
  10. package/dist/benchmark/shadow-mode.d.ts +17 -0
  11. package/dist/benchmark/shadow-mode.js +96 -0
  12. package/dist/cli/register-spec-agent-goal-commands.js +45 -0
  13. package/dist/cli/release-promotion-gate.d.ts +14 -0
  14. package/dist/cli/release-promotion-gate.js +71 -0
  15. package/dist/cli/v2/release-commands.d.ts +29 -0
  16. package/dist/cli/v2/release-commands.js +95 -0
  17. package/dist/commands/chat/native-root-loop.js +14 -1
  18. package/dist/commands/chat/slash/commands/session.js +19 -1
  19. package/dist/commands/goal-interview.d.ts +18 -0
  20. package/dist/commands/goal-interview.js +396 -0
  21. package/dist/commands/merge.js +102 -56
  22. package/dist/contracts/interview.d.ts +106 -0
  23. package/dist/contracts/interview.js +9 -0
  24. package/dist/contracts/provider-health.d.ts +37 -0
  25. package/dist/contracts/provider-health.js +49 -1
  26. package/dist/evidence/evidence-trust-score.d.ts +101 -0
  27. package/dist/evidence/evidence-trust-score.js +408 -0
  28. package/dist/evidence/index.d.ts +6 -0
  29. package/dist/evidence/index.js +3 -0
  30. package/dist/evidence/proof-trust-cli.d.ts +8 -0
  31. package/dist/evidence/proof-trust-cli.js +27 -0
  32. package/dist/evidence/proof-trust.d.ts +14 -0
  33. package/dist/evidence/proof-trust.js +381 -0
  34. package/dist/evidence/regression-proof-matrix.d.ts +42 -0
  35. package/dist/evidence/regression-proof-matrix.js +72 -0
  36. package/dist/goal/intent-frame.d.ts +6 -0
  37. package/dist/goal/intent-frame.js +21 -9
  38. package/dist/goal/interview-assimilation.d.ts +13 -0
  39. package/dist/goal/interview-assimilation.js +383 -0
  40. package/dist/goal/interview-question-bank.d.ts +11 -0
  41. package/dist/goal/interview-question-bank.js +225 -0
  42. package/dist/goal/interview-scoring.d.ts +31 -0
  43. package/dist/goal/interview-scoring.js +187 -0
  44. package/dist/goal/interview-session.d.ts +25 -0
  45. package/dist/goal/interview-session.js +116 -0
  46. package/dist/input/input-envelope.d.ts +22 -0
  47. package/dist/input/input-envelope.js +1 -0
  48. package/dist/orchestration/merge-arbiter.d.ts +91 -0
  49. package/dist/orchestration/merge-arbiter.js +376 -0
  50. package/dist/providers/health.d.ts +3 -0
  51. package/dist/providers/health.js +46 -0
  52. package/dist/providers/index.d.ts +1 -0
  53. package/dist/providers/index.js +1 -0
  54. package/dist/providers/provider-health.d.ts +8 -1
  55. package/dist/providers/provider-health.js +39 -0
  56. package/dist/providers/provider-task-runner.js +31 -0
  57. package/dist/providers/provider.d.ts +2 -0
  58. package/dist/providers/router.js +87 -3
  59. package/dist/providers/types.d.ts +4 -0
  60. package/dist/runtime/advanced-control-loop.d.ts +60 -0
  61. package/dist/runtime/advanced-control-loop.js +136 -0
  62. package/dist/runtime/agent-runtime.d.ts +10 -0
  63. package/dist/runtime/blast-radius.d.ts +10 -0
  64. package/dist/runtime/blast-radius.js +14 -0
  65. package/dist/runtime/contracts/evidence.d.ts +87 -0
  66. package/dist/runtime/contracts/evidence.js +7 -0
  67. package/dist/runtime/contracts/router-v2.d.ts +44 -0
  68. package/dist/runtime/contracts/router-v2.js +4 -0
  69. package/dist/runtime/contracts/weakness-remediation.d.ts +67 -0
  70. package/dist/runtime/contracts/weakness-remediation.js +36 -0
  71. package/dist/runtime/kimi-api-runtime.js +59 -1
  72. package/dist/runtime/proof-bundle-trust.d.ts +74 -0
  73. package/dist/runtime/proof-bundle-trust.js +100 -0
  74. package/dist/runtime/provider-maturity-gate.d.ts +43 -0
  75. package/dist/runtime/provider-maturity-gate.js +129 -0
  76. package/dist/runtime/public-surface.d.ts +93 -0
  77. package/dist/runtime/public-surface.js +146 -0
  78. package/dist/runtime/router-v2-scoring.d.ts +11 -0
  79. package/dist/runtime/router-v2-scoring.js +151 -0
  80. package/dist/runtime/tool-dispatch-contracts.d.ts +24 -3
  81. package/dist/runtime/tool-dispatch-contracts.js +42 -2
  82. package/dist/runtime/weakness-remediation-index.d.ts +27 -0
  83. package/dist/runtime/weakness-remediation-index.js +37 -0
  84. package/dist/safety/enforcement-engine.d.ts +89 -0
  85. package/dist/safety/enforcement-engine.js +279 -0
  86. package/dist/safety/tool-authority-gate.d.ts +40 -0
  87. package/dist/safety/tool-authority-gate.js +92 -0
  88. package/dist/schema/evidence.schema.d.ts +2 -2
  89. package/dist/schema/proof-bundle.schema.d.ts +28 -28
  90. package/dist/util/clipboard-image.d.ts +49 -0
  91. package/dist/util/clipboard-image.js +263 -0
  92. package/docs/2026-06-09/critical-issues.md +20 -0
  93. package/docs/2026-06-09/improvements.md +14 -0
  94. package/docs/2026-06-09/init-checklist.md +25 -0
  95. package/docs/2026-06-09/plan.md +20 -0
  96. package/docs/benchmark-design.md +122 -0
  97. package/docs/github-organic-promotion.md +127 -0
  98. package/docs/native-root-runtime-algorithms.md +301 -0
  99. package/package.json +8 -4
  100. package/readmeasset/ASSET_INDEX.md +1 -0
  101. package/templates/skills/agents/omk-agent-reach-websearch/SKILL.md +55 -0
  102. package/templates/skills/kimi/omk-agent-reach-websearch/SKILL.md +55 -0
@@ -0,0 +1,396 @@
1
+ import { mkdir, writeFile, readFile, readdir } from "fs/promises";
2
+ import { basename, join } from "path";
3
+ import { getProjectRoot } from "../util/fs.js";
4
+ import { style, header, status, label } from "../util/theme.js";
5
+ import { NotFoundError, UsageError, emitError } from "../util/cli-contract.js";
6
+ import { createGoalPersister } from "../goal/persistence.js";
7
+ import { createGoalSpec, updateGoalStatus } from "../goal/intake.js";
8
+ import { buildInterviewSession, ingestAnswers } from "../goal/interview-session.js";
9
+ import { applyInterviewDelta } from "../goal/interview-assimilation.js";
10
+ import { redactSecretText } from "../goal/intent-frame.js";
11
+ import { readImageFile } from "../util/clipboard-image.js";
12
+ const IMAGE_ATTACHMENT_QUESTION_ID = "q-image-attachment";
13
+ function getGoalBasePath() {
14
+ return join(getProjectRoot(), ".omk", "goals");
15
+ }
16
+ function printAlphaWarning(json) {
17
+ if (json)
18
+ return;
19
+ if (!process.env.OMK_GOAL_ALPHA) {
20
+ console.log(style.orange("⚠️ Goal interview is alpha. Set OMK_GOAL_ALPHA=1 to suppress this warning."));
21
+ }
22
+ }
23
+ function parseDepth(value) {
24
+ if (!value)
25
+ return undefined;
26
+ if (value === "light" || value === "standard" || value === "deep")
27
+ return value;
28
+ throw new UsageError(`Invalid depth: ${value} (expected light | standard | deep)`);
29
+ }
30
+ function parseMode(value) {
31
+ if (!value || value === "create")
32
+ return "create";
33
+ if (value === "refine")
34
+ return "refine";
35
+ throw new UsageError(`Invalid mode: ${value} (expected create | refine)`);
36
+ }
37
+ async function loadAnswersFile(root, filePath) {
38
+ const abs = join(root, filePath);
39
+ let content;
40
+ try {
41
+ content = await readFile(abs, "utf-8");
42
+ }
43
+ catch {
44
+ throw new NotFoundError(`Answers file not found: ${filePath}`);
45
+ }
46
+ let parsed;
47
+ try {
48
+ parsed = JSON.parse(content);
49
+ }
50
+ catch {
51
+ throw new UsageError(`Answers file is not valid JSON: ${filePath}`);
52
+ }
53
+ const rows = Array.isArray(parsed)
54
+ ? parsed
55
+ : (parsed && typeof parsed === "object" && Array.isArray(parsed.answers)
56
+ ? parsed.answers
57
+ : null);
58
+ if (!rows) {
59
+ throw new UsageError(`Answers file must be an array or { "answers": [...] }: ${filePath}`);
60
+ }
61
+ const now = new Date().toISOString();
62
+ const answers = [];
63
+ for (const row of rows) {
64
+ if (!row || typeof row !== "object")
65
+ continue;
66
+ const r = row;
67
+ if (typeof r.questionId !== "string")
68
+ continue;
69
+ answers.push({
70
+ questionId: r.questionId,
71
+ answer: typeof r.answer === "string" ? r.answer : "",
72
+ answeredAt: typeof r.answeredAt === "string" ? r.answeredAt : now,
73
+ skipped: r.skipped === true ? true : undefined,
74
+ });
75
+ }
76
+ return answers;
77
+ }
78
+ function buildImageAttachment(imagePath) {
79
+ const img = readImageFile(imagePath);
80
+ if (!img.ok) {
81
+ throw new UsageError(img.error ?? `Unable to read image file: ${imagePath}`);
82
+ }
83
+ if (typeof img.dataUri !== "string" || typeof img.ext !== "string") {
84
+ throw new UsageError(`Image file could not be converted to a data URI: ${imagePath}`);
85
+ }
86
+ return {
87
+ name: basename(imagePath),
88
+ path: imagePath,
89
+ mimeType: `image/${img.ext}`,
90
+ dataUri: img.dataUri,
91
+ ext: img.ext,
92
+ source: "file",
93
+ };
94
+ }
95
+ function attachImageToSession(session, imagePath) {
96
+ const attachment = buildImageAttachment(imagePath);
97
+ const answeredAt = new Date().toISOString();
98
+ return {
99
+ ...session,
100
+ updatedAt: answeredAt,
101
+ answers: [
102
+ ...session.answers.filter((answer) => answer.questionId !== IMAGE_ATTACHMENT_QUESTION_ID),
103
+ {
104
+ questionId: IMAGE_ATTACHMENT_QUESTION_ID,
105
+ answer: `Image file: ${imagePath}`,
106
+ answeredAt,
107
+ skipped: false,
108
+ },
109
+ ],
110
+ findings: [
111
+ ...session.findings.filter((finding) => finding.sourceQuestionId !== IMAGE_ATTACHMENT_QUESTION_ID),
112
+ {
113
+ field: "attachments",
114
+ value: attachment,
115
+ sourceQuestionId: IMAGE_ATTACHMENT_QUESTION_ID,
116
+ confidence: 1,
117
+ },
118
+ ],
119
+ };
120
+ }
121
+ function renderQuestionsMarkdown(session) {
122
+ const lines = [
123
+ `# Interview Questions — ${session.sessionId}`,
124
+ "",
125
+ `**Mode:** ${session.mode} | **Depth:** ${session.depth} | **Ambiguity:** ${session.ambiguity}`,
126
+ "",
127
+ "| # | Score | Req | Target | Kind | Prompt |",
128
+ "|---|-------|-----|--------|------|--------|",
129
+ ];
130
+ session.questions.forEach((q, i) => {
131
+ const req = q.required ? "✓" : "";
132
+ lines.push(`| ${i + 1} | ${q.score} | ${req} | ${q.targetField} | ${q.kind} | ${q.prompt.replace(/\|/g, "\\|")} |`);
133
+ });
134
+ lines.push("");
135
+ return lines.join("\n");
136
+ }
137
+ function renderReportMarkdown(session) {
138
+ const c = session.completeness;
139
+ const lines = [
140
+ `# Interview Report — ${session.sessionId}`,
141
+ "",
142
+ `**Status:** ${session.status} | **Mode:** ${session.mode} | **Depth:** ${session.depth}`,
143
+ `**Ambiguity:** ${session.ambiguity} | **Completeness:** ${c.overall}`,
144
+ "",
145
+ "## Completeness by axis",
146
+ "",
147
+ `- objective: ${c.objective}`,
148
+ `- successCriteria: ${c.successCriteria}`,
149
+ `- evidence: ${c.evidence}`,
150
+ `- artifacts: ${c.artifacts}`,
151
+ `- constraints: ${c.constraints}`,
152
+ `- risks: ${c.risks}`,
153
+ `- authority: ${c.authority}`,
154
+ "",
155
+ ];
156
+ if (c.criticalMissing.length > 0) {
157
+ lines.push("## Critical missing fields", "", ...c.criticalMissing.map((m) => `- ${m}`), "");
158
+ }
159
+ if (c.contradictions.length > 0) {
160
+ lines.push("## Unresolved contradictions", "", ...c.contradictions.map((m) => `- ${m}`), "");
161
+ }
162
+ lines.push("## Answers", "");
163
+ if (session.answers.length === 0) {
164
+ lines.push("_No answers recorded yet._", "");
165
+ }
166
+ else {
167
+ for (const a of session.answers) {
168
+ if (a.skipped) {
169
+ lines.push(`- **${a.questionId}**: _(skipped)_`);
170
+ }
171
+ else {
172
+ lines.push(`- **${a.questionId}**: ${a.answer}`);
173
+ }
174
+ }
175
+ lines.push("");
176
+ }
177
+ lines.push("## Spec delta", "");
178
+ if (session.specDelta.changes.length === 0) {
179
+ lines.push("_No structured changes derived._", "");
180
+ }
181
+ else {
182
+ for (const ch of session.specDelta.changes) {
183
+ lines.push(`- \`${ch.op}\` **${String(ch.field)}** (conf ${ch.confidence}): ${JSON.stringify(ch.value)}`);
184
+ }
185
+ lines.push("");
186
+ }
187
+ return lines.join("\n");
188
+ }
189
+ async function writeSessionArtifacts(dir, session) {
190
+ await mkdir(dir, { recursive: true });
191
+ await writeFile(join(dir, "interview.json"), `${JSON.stringify(session, null, 2)}\n`);
192
+ await writeFile(join(dir, "spec-delta.json"), `${JSON.stringify(session.specDelta, null, 2)}\n`);
193
+ await writeFile(join(dir, "questions.md"), renderQuestionsMarkdown(session));
194
+ await writeFile(join(dir, "interview-report.md"), renderReportMarkdown(session));
195
+ const answersJsonl = session.answers.map((a) => JSON.stringify(a)).join("\n");
196
+ await writeFile(join(dir, "answers.jsonl"), answersJsonl.length > 0 ? `${answersJsonl}\n` : "");
197
+ }
198
+ function printSessionSummary(session, sessionDir) {
199
+ console.log(header("Goal Interview"));
200
+ console.log(label("Session", session.sessionId));
201
+ console.log(label("Mode", session.mode));
202
+ console.log(label("Depth", session.depth));
203
+ console.log(label("Ambiguity", String(session.ambiguity)));
204
+ console.log(label("Completeness", String(session.completeness.overall)));
205
+ console.log(label("Status", session.status));
206
+ console.log(label("Questions", String(session.questions.length)));
207
+ console.log(label("Answers", String(session.answers.length)));
208
+ if (session.completeness.criticalMissing.length > 0) {
209
+ console.log(label("Missing", session.completeness.criticalMissing.join(", ")));
210
+ }
211
+ if (session.completeness.contradictions.length > 0) {
212
+ console.log(style.pink(`Contradictions: ${session.completeness.contradictions.join("; ")}`));
213
+ }
214
+ console.log("");
215
+ if (session.answers.length === 0) {
216
+ console.log(style.purpleBold("Top questions"));
217
+ for (const q of session.questions.slice(0, 6)) {
218
+ const req = q.required ? style.pink("[required]") : style.gray("[optional]");
219
+ console.log(` ${req} (${q.score}) ${q.prompt}`);
220
+ }
221
+ console.log("");
222
+ console.log(style.gray("Answer via: omk goal interview \"<prompt>\" --answers answers.json --write-spec"));
223
+ }
224
+ console.log(status.success(`interview.json → ${join(sessionDir, "interview.json")}`));
225
+ }
226
+ export async function goalInterviewCommand(input, options) {
227
+ printAlphaWarning(options.json);
228
+ const root = getProjectRoot();
229
+ const persister = createGoalPersister(getGoalBasePath());
230
+ const mode = parseMode(options.mode);
231
+ const depth = parseDepth(options.depth);
232
+ const maxQuestions = options.maxQuestions ? Number.parseInt(options.maxQuestions, 10) : undefined;
233
+ if (maxQuestions !== undefined && (!Number.isFinite(maxQuestions) || maxQuestions <= 0)) {
234
+ throw new UsageError(`Invalid --max-questions: ${options.maxQuestions}`);
235
+ }
236
+ // Resolve existing goal for refine mode or when --goal-id / id positional is given.
237
+ const goalIdHint = options.goalId ?? (mode === "refine" ? input : undefined);
238
+ let existingGoal = null;
239
+ if (goalIdHint) {
240
+ existingGoal = await persister.load(goalIdHint);
241
+ if (!existingGoal) {
242
+ const msg = `Goal not found: ${goalIdHint}`;
243
+ emitError(msg, Boolean(options.json));
244
+ throw new NotFoundError(msg);
245
+ }
246
+ }
247
+ const rawPrompt = existingGoal
248
+ ? (existingGoal.rawPrompt || existingGoal.objective || existingGoal.title)
249
+ : (input ?? "");
250
+ if (!rawPrompt.trim()) {
251
+ throw new UsageError("A raw goal prompt or an existing --goal-id is required.");
252
+ }
253
+ // Redact secrets from the prompt before it reaches the session, the GoalSpec,
254
+ // persisted artifacts, or --json output.
255
+ const safePrompt = redactSecretText(rawPrompt);
256
+ const seed = {
257
+ rawPrompt: safePrompt,
258
+ riskLevel: existingGoal?.riskLevel,
259
+ goal: existingGoal ?? undefined,
260
+ };
261
+ let session = buildInterviewSession({
262
+ seed,
263
+ mode: existingGoal ? "refine" : mode,
264
+ depth,
265
+ maxQuestions,
266
+ goalId: existingGoal?.goalId,
267
+ });
268
+ if (options.answers) {
269
+ const answers = await loadAnswersFile(root, options.answers);
270
+ session = ingestAnswers(session, seed, answers);
271
+ }
272
+ if (options.image) {
273
+ session = attachImageToSession(session, options.image);
274
+ }
275
+ // Optionally apply the delta to a GoalSpec.
276
+ let savedGoalId = existingGoal?.goalId;
277
+ if (options.writeSpec) {
278
+ let goal;
279
+ if (existingGoal) {
280
+ const result = applyInterviewDelta(existingGoal, session.specDelta);
281
+ goal = updateGoalStatus(result.goal, result.goal.status, { planRevision: result.goal.planRevision + 1 });
282
+ }
283
+ else {
284
+ const base = createGoalSpec(safePrompt);
285
+ goal = applyInterviewDelta(base, session.specDelta).goal;
286
+ }
287
+ await persister.save(goal);
288
+ await persister.appendHistory(goal.goalId, {
289
+ at: new Date().toISOString(),
290
+ action: existingGoal ? "interview-refine" : "interview-create",
291
+ detail: {
292
+ sessionId: session.sessionId,
293
+ ambiguity: session.ambiguity,
294
+ completeness: session.completeness.overall,
295
+ changes: session.specDelta.changes.length,
296
+ },
297
+ });
298
+ savedGoalId = goal.goalId;
299
+ session.goalId = goal.goalId;
300
+ session.specDelta.goalId = goal.goalId;
301
+ }
302
+ // Persist interview artifacts. With a goal id, store under the goal dir.
303
+ const sessionDir = savedGoalId
304
+ ? join(getGoalBasePath(), savedGoalId, "interviews", session.sessionId)
305
+ : join(root, ".omk", "interviews", session.sessionId);
306
+ await writeSessionArtifacts(sessionDir, session);
307
+ if (options.json) {
308
+ console.log(JSON.stringify(session, null, 2));
309
+ return;
310
+ }
311
+ printSessionSummary(session, sessionDir);
312
+ if (savedGoalId) {
313
+ console.log(status.success(`goal spec written → ${savedGoalId}`));
314
+ console.log(style.gray(`Next: omk goal plan ${savedGoalId}`));
315
+ }
316
+ }
317
+ async function findLatestInterviewSessionId(goalDir) {
318
+ const interviewsDir = join(goalDir, "interviews");
319
+ try {
320
+ const entries = await readdir(interviewsDir, { withFileTypes: true });
321
+ const dirs = entries.filter((e) => e.isDirectory()).map((e) => e.name).sort();
322
+ return dirs.length > 0 ? dirs[dirs.length - 1] : null;
323
+ }
324
+ catch {
325
+ return null;
326
+ }
327
+ }
328
+ export async function goalRefineCommand(goalId, options) {
329
+ printAlphaWarning(options.json);
330
+ const persister = createGoalPersister(getGoalBasePath());
331
+ const goal = await persister.load(goalId);
332
+ if (!goal) {
333
+ const msg = `Goal not found: ${goalId}`;
334
+ emitError(msg, Boolean(options.json));
335
+ throw new NotFoundError(msg);
336
+ }
337
+ const goalDir = join(getGoalBasePath(), goalId);
338
+ const from = options.fromInterview ?? "latest";
339
+ const sessionId = from === "latest" ? await findLatestInterviewSessionId(goalDir) : from;
340
+ if (!sessionId) {
341
+ const msg = `No interview session found for goal: ${goalId}. Run: omk goal interview ${goalId} --mode refine --answers answers.json`;
342
+ emitError(msg, Boolean(options.json));
343
+ throw new NotFoundError(msg);
344
+ }
345
+ const deltaPath = join(goalDir, "interviews", sessionId, "spec-delta.json");
346
+ let delta;
347
+ try {
348
+ delta = JSON.parse(await readFile(deltaPath, "utf-8"));
349
+ }
350
+ catch {
351
+ const msg = `Interview spec-delta not found: ${deltaPath}`;
352
+ emitError(msg, Boolean(options.json));
353
+ throw new NotFoundError(msg);
354
+ }
355
+ const result = applyInterviewDelta(goal, delta);
356
+ const refined = updateGoalStatus(result.goal, result.goal.status, { planRevision: result.goal.planRevision + 1 });
357
+ await persister.save(refined);
358
+ await persister.appendHistory(goalId, {
359
+ at: new Date().toISOString(),
360
+ action: "refine",
361
+ detail: {
362
+ sessionId,
363
+ applied: result.appliedChanges.length,
364
+ skipped: result.skippedChanges.length,
365
+ contradictions: result.contradictions.length,
366
+ planRevision: refined.planRevision,
367
+ },
368
+ });
369
+ if (options.plan) {
370
+ const { goalPlanCommand } = await import("./goal.js");
371
+ await goalPlanCommand(goalId, { json: options.json });
372
+ return;
373
+ }
374
+ if (options.json) {
375
+ console.log(JSON.stringify({
376
+ goalId,
377
+ sessionId,
378
+ applied: result.appliedChanges.length,
379
+ skipped: result.skippedChanges.length,
380
+ contradictions: result.contradictions,
381
+ planRevision: refined.planRevision,
382
+ status: refined.status,
383
+ }, null, 2));
384
+ return;
385
+ }
386
+ console.log(header("Goal Refined"));
387
+ console.log(label("ID", goalId));
388
+ console.log(label("From interview", sessionId));
389
+ console.log(label("Applied changes", String(result.appliedChanges.length)));
390
+ console.log(label("Skipped changes", String(result.skippedChanges.length)));
391
+ console.log(label("Plan revision", String(refined.planRevision)));
392
+ if (result.contradictions.length > 0) {
393
+ console.log(style.pink(`Contradictions: ${result.contradictions.join("; ")}`));
394
+ }
395
+ console.log(style.gray(`Next: omk goal plan ${goalId}`));
396
+ }
@@ -11,6 +11,7 @@ import { getOmkResourceSettings } from "../util/resource-profile.js";
11
11
  import { defaultScopedRoleAgentFile, writeScopedAgentFile } from "../util/scoped-agent-file.js";
12
12
  import { createOmkJsonEnvelope } from "../util/json-envelope.js";
13
13
  import { emitJson } from "../util/cli-contract.js";
14
+ import { runMergeArbiter, } from "../orchestration/merge-arbiter.js";
14
15
  /**
15
16
  * JSON path for `omk merge --json`.
16
17
  * Read-only preview: resolves the run, collects worktree diffs (git diff +
@@ -138,65 +139,110 @@ export async function mergeCommand(options) {
138
139
  if (dryRun)
139
140
  console.log(style.orange("🟡 DRY RUN — no changes will be applied"));
140
141
  console.log("");
141
- // ── 1. Collect diffs from all worktrees ──
142
- const workers = [];
143
- for (const name of workerNames) {
144
- const wtPath = join(worktreesDir, name);
145
- const diffResult = await runShell("git", ["-C", wtPath, "diff", currentBranch], { timeout: 15000 });
146
- if (diffResult.failed || !diffResult.stdout.trim()) {
147
- console.log(style.gray(` ${name}: no changes`));
148
- continue;
149
- }
150
- const diff = diffResult.stdout;
151
- const diffLines = diff.split("\n").length;
152
- // Check apply-ability
153
- const applyCheck = await runShell("git", ["apply", "--check"], {
154
- cwd: root,
155
- input: diff,
156
- timeout: 15000,
142
+ let report;
143
+ let winner = null;
144
+ if (strategy === "arbiter") {
145
+ // ── Arbiter path ──
146
+ console.log(style.purple("Running merge arbiter..."));
147
+ const config = await readTextFile(join(root, ".omk", "config.toml"), "");
148
+ const arbiterResult = await runMergeArbiter(worktreesDir, currentBranch, root, config, {
149
+ threshold: 0.6,
150
+ testTimeoutMs: 120_000,
157
151
  });
158
- const canApply = !applyCheck.failed;
159
- workers.push({ name, path: wtPath, diff, diffLines, canApply });
160
- console.log(` ${style.purpleBold(name)} ${canApply ? style.mint("(clean)") : style.pink("(conflicts)")} ${style.gray(`${diffLines} lines`)}`);
161
- }
162
- if (workers.length === 0) {
163
- console.log(status.warn("No worker changes to merge."));
164
- return;
165
- }
166
- // ── 2. Reviewer scoring ──
167
- console.log("");
168
- console.log(style.purple("Scoring diffs with reviewer..."));
169
- for (const w of workers) {
170
- const score = await scoreDiff(w.diff, w.name);
171
- w.reviewScore = score.score;
172
- w.reviewReason = score.reason;
173
- const color = score.score >= 80 ? style.mint : score.score >= 50 ? style.orange : style.pink;
174
- console.log(` ${w.name}: ${color(`${score.score}/100`)} ${style.gray(score.reason)}`);
175
- }
176
- // ── 3. Test verification in worktrees ──
177
- console.log("");
178
- console.log(style.purple("Running tests in worktrees..."));
179
- for (const w of workers) {
180
- const testResult = await runShell("sh", ["-c", "npm test 2>/dev/null || pnpm test 2>/dev/null || yarn test 2>/dev/null || true"], {
181
- cwd: w.path,
182
- timeout: 120_000,
152
+ // Map arbiter candidates back to WorkerDiff for reporting
153
+ const arbiterWorkers = arbiterResult.trace.steps
154
+ .filter((s) => s.step === "evidence-suite" || s.step === "score")
155
+ .map((s) => {
156
+ return {
157
+ name: s.candidateId.replace("candidate-", ""),
158
+ path: "",
159
+ diff: "",
160
+ diffLines: 0,
161
+ canApply: s.detail.includes("apply=true"),
162
+ reviewScore: 50,
163
+ reviewReason: s.detail,
164
+ testsPassed: s.detail.includes("tests=true"),
165
+ };
183
166
  });
184
- w.testsPassed = !testResult.failed;
185
- console.log(` ${w.name}: ${w.testsPassed ? style.mint("tests passed") : style.pink("tests failed")}`);
167
+ // De-duplicate by name
168
+ const workerMap = new Map();
169
+ for (const w of arbiterWorkers)
170
+ workerMap.set(w.name, w);
171
+ report = {
172
+ winner: arbiterResult.winner?.name ?? null,
173
+ reason: arbiterResult.rationale.summary,
174
+ conflicts: arbiterResult.rationale.conflicts,
175
+ filesApplied: 0,
176
+ dryRun,
177
+ workers: [...workerMap.values()],
178
+ };
179
+ if (arbiterResult.requiresHumanApproval) {
180
+ console.log(status.error(arbiterResult.rationale.humanApprovalReason ?? "No candidate meets threshold — human approval required."));
181
+ printReport(report);
182
+ process.exit(1);
183
+ }
184
+ winner = arbiterResult.winner ? { name: arbiterResult.winner.name, path: arbiterResult.winner.path, diff: arbiterResult.winner.diff, diffLines: arbiterResult.winner.diffLines, canApply: arbiterResult.winner.canApply, reviewScore: arbiterResult.winner.evidence.reviewerScore, reviewReason: arbiterResult.winner.evidence.reviewerReason, testsPassed: arbiterResult.winner.evidence.testsPassed } : null;
185
+ }
186
+ else {
187
+ // ── 1. Collect diffs from all worktrees ──
188
+ const workers = [];
189
+ for (const name of workerNames) {
190
+ const wtPath = join(worktreesDir, name);
191
+ const diffResult = await runShell("git", ["-C", wtPath, "diff", currentBranch], { timeout: 15000 });
192
+ if (diffResult.failed || !diffResult.stdout.trim()) {
193
+ console.log(style.gray(` ${name}: no changes`));
194
+ continue;
195
+ }
196
+ const diff = diffResult.stdout;
197
+ const diffLines = diff.split("\n").length;
198
+ // Check apply-ability
199
+ const applyCheck = await runShell("git", ["apply", "--check"], {
200
+ cwd: root,
201
+ input: diff,
202
+ timeout: 15000,
203
+ });
204
+ const canApply = !applyCheck.failed;
205
+ workers.push({ name, path: wtPath, diff, diffLines, canApply });
206
+ console.log(` ${style.purpleBold(name)} ${canApply ? style.mint("(clean)") : style.pink("(conflicts)")} ${style.gray(`${diffLines} lines`)}`);
207
+ }
208
+ if (workers.length === 0) {
209
+ console.log(status.warn("No worker changes to merge."));
210
+ return;
211
+ }
212
+ // ── 2. Reviewer scoring ──
213
+ console.log("");
214
+ console.log(style.purple("Scoring diffs with reviewer..."));
215
+ for (const w of workers) {
216
+ const score = await scoreDiff(w.diff, w.name);
217
+ w.reviewScore = score.score;
218
+ w.reviewReason = score.reason;
219
+ const color = score.score >= 80 ? style.mint : score.score >= 50 ? style.orange : style.pink;
220
+ console.log(` ${w.name}: ${color(`${score.score}/100`)} ${style.gray(score.reason)}`);
221
+ }
222
+ // ── 3. Test verification in worktrees ──
223
+ console.log("");
224
+ console.log(style.purple("Running tests in worktrees..."));
225
+ for (const w of workers) {
226
+ const testResult = await runShell("sh", ["-c", "npm test 2>/dev/null || pnpm test 2>/dev/null || yarn test 2>/dev/null || true"], {
227
+ cwd: w.path,
228
+ timeout: 120_000,
229
+ });
230
+ w.testsPassed = !testResult.failed;
231
+ console.log(` ${w.name}: ${w.testsPassed ? style.mint("tests passed") : style.pink("tests failed")}`);
232
+ }
233
+ // ── 4. Select winner ──
234
+ console.log("");
235
+ console.log(style.purple("Selecting winner..."));
236
+ winner = selectWinner(workers, strategy);
237
+ report = {
238
+ winner: winner?.name ?? null,
239
+ reason: winner?.reviewReason ?? "No suitable candidate",
240
+ conflicts: workers.filter((w) => !w.canApply).map((w) => w.name),
241
+ filesApplied: 0,
242
+ dryRun,
243
+ workers,
244
+ };
186
245
  }
187
- // ── 4. Select winner ──
188
- console.log("");
189
- console.log(style.purple("Selecting winner..."));
190
- const winner = selectWinner(workers, strategy);
191
- // ── 5. Apply or preview ──
192
- const report = {
193
- winner: winner?.name ?? null,
194
- reason: winner?.reviewReason ?? "No suitable candidate",
195
- conflicts: workers.filter((w) => !w.canApply).map((w) => w.name),
196
- filesApplied: 0,
197
- dryRun,
198
- workers,
199
- };
200
246
  if (!winner) {
201
247
  console.log(status.error("No worker diff can be applied cleanly."));
202
248
  printReport(report);
@@ -0,0 +1,106 @@
1
+ import type { GoalSpec, RiskLevel } from "./goal.js";
2
+ export declare const INTERVIEW_SCHEMA_VERSION: "omk.interview.v1";
3
+ export declare const INTERVIEW_DELTA_SCHEMA_VERSION: "omk.interview-delta.v1";
4
+ export type InterviewDepth = "light" | "standard" | "deep";
5
+ export type InterviewMode = "create" | "refine";
6
+ export type InterviewQuestionKind = "objective" | "success-criteria" | "artifact" | "constraint" | "non-goal" | "risk" | "authority" | "evidence" | "dependency" | "rollback";
7
+ /**
8
+ * Field on GoalSpec (or a derived structure) that an answer is meant to fill.
9
+ * A question without a targetField is low quality and must be dropped.
10
+ */
11
+ export type InterviewTargetField = "objective" | "successCriteria" | "expectedArtifacts" | "constraints" | "nonGoals" | "risks" | "riskLevel" | "intentFrame" | "actionAtoms";
12
+ export interface InterviewQuestion {
13
+ id: string;
14
+ kind: InterviewQuestionKind;
15
+ prompt: string;
16
+ required: boolean;
17
+ targetField: InterviewTargetField;
18
+ /** 0..1 — how much answering reduces uncertainty about the goal. */
19
+ informationGain: number;
20
+ /** 0..1 — how much answering reduces execution risk. */
21
+ riskReduction: number;
22
+ /** 0..1 — how strongly the answer reshapes the execution DAG. */
23
+ dagImpact: number;
24
+ /** 0..1 — how strongly the answer strengthens evidence gates. */
25
+ evidenceImpact: number;
26
+ /** 0..1 — burden on the user (penalty). */
27
+ userCost: number;
28
+ /** Derived ranking score, 0..1, rounded to 2 decimals. */
29
+ score: number;
30
+ }
31
+ export interface InterviewAnswer {
32
+ questionId: string;
33
+ answer: string;
34
+ answeredAt: string;
35
+ skipped?: boolean;
36
+ }
37
+ export interface InterviewFinding {
38
+ field: InterviewTargetField | string;
39
+ value: unknown;
40
+ sourceQuestionId: string;
41
+ /** 0..1 confidence that this finding is correct and explicit. */
42
+ confidence: number;
43
+ /** True when this finding contradicts an existing explicit value. */
44
+ conflict?: boolean;
45
+ }
46
+ export interface InterviewCompleteness {
47
+ /** Weighted overall completeness, 0..1. */
48
+ overall: number;
49
+ objective: number;
50
+ successCriteria: number;
51
+ evidence: number;
52
+ artifacts: number;
53
+ constraints: number;
54
+ risks: number;
55
+ authority: number;
56
+ /** Required fields with no usable value yet. */
57
+ criticalMissing: string[];
58
+ /** Unresolved contradictions between answers and existing values. */
59
+ contradictions: string[];
60
+ }
61
+ export type InterviewDeltaField = keyof GoalSpec | "successCriteria" | "expectedArtifacts" | "riskLevel";
62
+ export interface InterviewSpecChange {
63
+ field: InterviewDeltaField;
64
+ op: "add" | "replace" | "remove";
65
+ value: unknown;
66
+ sourceQuestionId: string;
67
+ /** 0..1 confidence used by the conflict resolver. */
68
+ confidence: number;
69
+ }
70
+ export interface InterviewSpecDelta {
71
+ schemaVersion: typeof INTERVIEW_DELTA_SCHEMA_VERSION;
72
+ goalId?: string;
73
+ changes: InterviewSpecChange[];
74
+ }
75
+ export type InterviewStatus = "open" | "complete" | "blocked";
76
+ export interface InterviewSession {
77
+ schemaVersion: typeof INTERVIEW_SCHEMA_VERSION;
78
+ sessionId: string;
79
+ goalId?: string;
80
+ mode: InterviewMode;
81
+ depth: InterviewDepth;
82
+ createdAt: string;
83
+ updatedAt: string;
84
+ rawPrompt?: string;
85
+ /** Ambiguity score 0..1; higher means more interview is needed. */
86
+ ambiguity: number;
87
+ questions: InterviewQuestion[];
88
+ answers: InterviewAnswer[];
89
+ findings: InterviewFinding[];
90
+ completeness: InterviewCompleteness;
91
+ specDelta: InterviewSpecDelta;
92
+ status: InterviewStatus;
93
+ }
94
+ /** Inputs the question bank needs to derive candidate questions. */
95
+ export interface InterviewSeed {
96
+ rawPrompt: string;
97
+ riskLevel?: RiskLevel;
98
+ goal?: GoalSpec;
99
+ }
100
+ /** Result of applying a spec delta to a GoalSpec. */
101
+ export interface InterviewApplyResult {
102
+ goal: GoalSpec;
103
+ appliedChanges: InterviewSpecChange[];
104
+ skippedChanges: InterviewSpecChange[];
105
+ contradictions: string[];
106
+ }