open-multi-agent-kit 0.78.0 → 0.78.1

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 (70) hide show
  1. package/CHANGELOG.md +44 -15
  2. package/MATURITY.md +2 -2
  3. package/README.md +56 -26
  4. package/ROADMAP.md +36 -28
  5. package/dist/cli/register-basic-commands.js +3 -2
  6. package/dist/cli/register-mcp-dag-cron-screenshot-commands.js +2 -0
  7. package/dist/cli/register-tool-commands.js +11 -0
  8. package/dist/cli/register-workflow-commands.js +1 -0
  9. package/dist/cli/registry/tooling.js +3 -2
  10. package/dist/commands/chat/core.js +5 -0
  11. package/dist/commands/chat/native-root-loop.js +60 -0
  12. package/dist/commands/dag-from-spec.d.ts +1 -0
  13. package/dist/commands/dag-from-spec.js +61 -1
  14. package/dist/commands/graph.d.ts +62 -0
  15. package/dist/commands/graph.js +182 -0
  16. package/dist/commands/merge.d.ts +1 -0
  17. package/dist/commands/merge.js +88 -0
  18. package/dist/commands/parallel/core.js +3 -3
  19. package/dist/commands/provider.js +5 -3
  20. package/dist/commands/star.js +6 -1
  21. package/dist/commands/summary.d.ts +4 -1
  22. package/dist/commands/summary.js +103 -1
  23. package/dist/commands/team.d.ts +1 -0
  24. package/dist/commands/team.js +38 -0
  25. package/dist/contracts/provider-health.d.ts +42 -0
  26. package/dist/contracts/provider-health.js +9 -0
  27. package/dist/goal/intent-frame.d.ts +24 -0
  28. package/dist/goal/intent-frame.js +18 -0
  29. package/dist/memory/local-graph-memory-store.d.ts +15 -0
  30. package/dist/memory/local-graph-memory-store.js +176 -0
  31. package/dist/memory/memory-store.d.ts +18 -0
  32. package/dist/memory/memory-store.js +18 -0
  33. package/dist/orchestration/adaptorch-topology.d.ts +59 -0
  34. package/dist/orchestration/adaptorch-topology.js +194 -0
  35. package/dist/orchestration/capability-routing.d.ts +23 -0
  36. package/dist/orchestration/capability-routing.js +56 -0
  37. package/dist/orchestration/dag-compiler-types.d.ts +3 -0
  38. package/dist/orchestration/dag-compiler.js +14 -1
  39. package/dist/orchestration/parallel-orchestrator.d.ts +6 -0
  40. package/dist/orchestration/parallel-orchestrator.js +31 -0
  41. package/dist/providers/provider-health.d.ts +39 -0
  42. package/dist/providers/provider-health.js +161 -0
  43. package/dist/runtime/context-broker.d.ts +13 -4
  44. package/dist/runtime/context-broker.js +14 -1
  45. package/dist/runtime/headroom-policy.d.ts +37 -0
  46. package/dist/runtime/headroom-policy.js +122 -0
  47. package/dist/runtime/ouroboros-policy.d.ts +57 -0
  48. package/dist/runtime/ouroboros-policy.js +134 -0
  49. package/dist/runtime/runtime-backed-task-runner.js +9 -1
  50. package/dist/runtime/tool-dispatch-contracts.d.ts +57 -1
  51. package/dist/runtime/tool-dispatch-contracts.js +79 -3
  52. package/dist/safety/tool-authority-gate.d.ts +62 -0
  53. package/dist/safety/tool-authority-gate.js +108 -0
  54. package/dist/schema/provider.schema.d.ts +4 -4
  55. package/dist/util/first-run-star.d.ts +9 -0
  56. package/dist/util/first-run-star.js +42 -1
  57. package/dist/util/terminal-input.d.ts +20 -0
  58. package/dist/util/terminal-input.js +32 -0
  59. package/dist/util/update-check.d.ts +6 -1
  60. package/dist/util/update-check.js +35 -1
  61. package/docs/2026-06-08/critical-issues.md +20 -0
  62. package/docs/2026-06-08/improvements.md +14 -0
  63. package/docs/2026-06-08/init-checklist.md +25 -0
  64. package/docs/2026-06-08/plan.md +20 -0
  65. package/docs/getting-started.md +31 -3
  66. package/docs/integrations/ouroboros.md +96 -0
  67. package/docs/provider-maturity.md +1 -1
  68. package/docs/versioning.md +3 -3
  69. package/package.json +1 -1
  70. package/dist/native/linux-x64/omk-safety +0 -0
@@ -3,7 +3,9 @@ import { join } from "path";
3
3
  import { writeFile, mkdir } from "fs/promises";
4
4
  import { getProjectRoot, pathExists, getRunPath, getRunsDir } from "../util/fs.js";
5
5
  import { header, label } from "../util/theme.js";
6
- import { createDag } from "../orchestration/dag.js";
6
+ import { createDag, getExecutionWaves } from "../orchestration/dag.js";
7
+ import { createOmkJsonEnvelope } from "../util/json-envelope.js";
8
+ import { emitJson } from "../util/cli-contract.js";
7
9
  function inferRole(description, explicitRole) {
8
10
  if (explicitRole)
9
11
  return explicitRole;
@@ -243,6 +245,61 @@ export async function loadSpecDag(specDir, options = {}) {
243
245
  }
244
246
  return tasksToDag(tasks, { parallel: options.parallel });
245
247
  }
248
+ /**
249
+ * Project the in-memory DAG artifact into the envelope `data` shape. The inner
250
+ * dag artifact (nodes) is preserved verbatim; edges/batches/stats are derived
251
+ * from the existing dependsOn graph (no new data sources).
252
+ */
253
+ function buildDagJsonData(inputId, dag) {
254
+ const edges = dag.nodes.flatMap((node) => node.dependsOn.map((from) => ({ from, to: node.id })));
255
+ const batches = getExecutionWaves(dag).map((wave) => wave.map((node) => node.id));
256
+ return {
257
+ inputId,
258
+ nodes: dag.nodes,
259
+ edges,
260
+ batches,
261
+ stats: { nodes: dag.nodes.length, edges: edges.length, batches: batches.length },
262
+ };
263
+ }
264
+ /**
265
+ * JSON path for `omk dag from-spec --json`.
266
+ * Emits EXACTLY ONE omk.contract.v1 envelope to stdout (no banner, no ANSI) and
267
+ * never throws on a missing/empty tasks.md, so JSON stdout stays one envelope.
268
+ */
269
+ async function emitDagFromSpecJson(targetDir, options) {
270
+ const started = Date.now();
271
+ let dag;
272
+ try {
273
+ dag = await loadSpecDag(targetDir, { parallel: options.parallel });
274
+ }
275
+ catch (err) {
276
+ const message = err instanceof Error ? err.message : String(err);
277
+ const empty = { nodes: [] };
278
+ emitJson(createOmkJsonEnvelope({
279
+ command: "dag",
280
+ status: "not-applicable",
281
+ ok: false,
282
+ data: buildDagJsonData(targetDir, empty),
283
+ warnings: [
284
+ { code: "RUN_ARTIFACT_MISSING", message, recoverable: true, severity: "warning" },
285
+ ],
286
+ durationMs: Date.now() - started,
287
+ }));
288
+ return empty;
289
+ }
290
+ if (options.output) {
291
+ await mkdir(getRunsDir(getProjectRoot()), { recursive: true });
292
+ await writeFile(options.output, JSON.stringify(dag, null, 2));
293
+ }
294
+ emitJson(createOmkJsonEnvelope({
295
+ command: "dag",
296
+ status: "passed",
297
+ ok: true,
298
+ data: buildDagJsonData(targetDir, dag),
299
+ durationMs: Date.now() - started,
300
+ }));
301
+ return dag;
302
+ }
246
303
  export async function dagFromSpecCommand(specDir, options = {}) {
247
304
  const root = getProjectRoot();
248
305
  let targetDir = specDir;
@@ -266,6 +323,9 @@ export async function dagFromSpecCommand(specDir, options = {}) {
266
323
  targetDir = getRunPath(options.run, undefined, root);
267
324
  }
268
325
  }
326
+ if (options.json === true || process.argv.includes("--json")) {
327
+ return emitDagFromSpecJson(targetDir, { output: options.output, parallel: options.parallel });
328
+ }
269
329
  const dag = await loadSpecDag(targetDir, { parallel: options.parallel });
270
330
  const dagJson = JSON.stringify(dag, null, 2);
271
331
  if (options.output) {
@@ -1,3 +1,5 @@
1
+ import type { RunManifest } from "../contracts/run.js";
2
+ import type { GraphState } from "../memory/local-graph-memory-store.js";
1
3
  export interface GraphViewCommandOptions {
2
4
  input?: string;
3
5
  output?: string;
@@ -7,3 +9,63 @@ export interface GraphViewCommandOptions {
7
9
  open?: boolean;
8
10
  }
9
11
  export declare function graphViewCommand(options?: GraphViewCommandOptions): Promise<void>;
12
+ export type GraphAuditVerdict = "passed" | "partial" | "failed";
13
+ export interface GraphAuditCounts {
14
+ providerRoute: number;
15
+ provider: number;
16
+ evidence: number;
17
+ decision: number;
18
+ artifact: number;
19
+ }
20
+ export interface GraphAuditMismatch {
21
+ field: string;
22
+ graph: number | string | null;
23
+ manifest: number | string | null;
24
+ }
25
+ export interface GraphAuditDangler {
26
+ edgeId: string;
27
+ type: string;
28
+ from: string;
29
+ to: string;
30
+ missing: "from" | "to";
31
+ }
32
+ export interface GraphAuditRunResult {
33
+ runId: string;
34
+ runNodeFound: boolean;
35
+ orphan: boolean;
36
+ manifestPresent: boolean;
37
+ counts: GraphAuditCounts;
38
+ mismatches: GraphAuditMismatch[];
39
+ danglers: GraphAuditDangler[];
40
+ verdict: GraphAuditVerdict;
41
+ notes: string[];
42
+ }
43
+ export interface GraphAuditReport {
44
+ schemaVersion: "omk.graph-audit.v1";
45
+ verdict: GraphAuditVerdict;
46
+ statePath?: string;
47
+ generatedAt: string;
48
+ summary: {
49
+ total: number;
50
+ passed: number;
51
+ partial: number;
52
+ failed: number;
53
+ };
54
+ runs: GraphAuditRunResult[];
55
+ }
56
+ export interface GraphAuditCommandOptions {
57
+ run?: string;
58
+ input?: string;
59
+ json?: boolean;
60
+ }
61
+ /**
62
+ * Pure auditor: cross-checks linked run subgraphs against their manifests.
63
+ * For each run it asserts the Run node exists with >=1 provider-route, provider,
64
+ * evidence, decision, and artifact edge, cross-checks evidence counts/artifact
65
+ * counts/status against the manifest, and flags orphan runs and dangling edges.
66
+ */
67
+ export declare function auditGraphRuns(state: GraphState, entries: ReadonlyArray<{
68
+ runId: string;
69
+ manifest?: RunManifest | null;
70
+ }>, statePath?: string): GraphAuditReport;
71
+ export declare function graphAuditCommand(options?: GraphAuditCommandOptions): Promise<void>;
@@ -1,7 +1,11 @@
1
+ import { readFile } from "fs/promises";
1
2
  import { join, resolve } from "path";
2
3
  import { getProjectRoot } from "../util/fs.js";
3
4
  import { createGraphView } from "../memory/graph-viewer.js";
4
5
  import { header, label, status } from "../util/theme.js";
6
+ import { createOmkJsonEnvelope } from "../util/json-envelope.js";
7
+ import { getRunArtifactPath, listValidRunIds, validateRunId } from "../util/run-store.js";
8
+ import { RunManifestSchema } from "../schema/run-manifest.schema.js";
5
9
  export async function graphViewCommand(options = {}) {
6
10
  const root = getProjectRoot();
7
11
  const inputPath = options.input ? resolve(root, options.input) : join(root, ".omk", "memory", "graph-state.json");
@@ -24,3 +28,181 @@ export async function graphViewCommand(options = {}) {
24
28
  console.log(label("Edges", String(result.edgeCount)));
25
29
  console.log(status.ok("Graph HTML generated"));
26
30
  }
31
+ const EVIDENCE_SUMMARY_FIELDS = ["required", "passed", "failed", "missing"];
32
+ /**
33
+ * Pure auditor: cross-checks linked run subgraphs against their manifests.
34
+ * For each run it asserts the Run node exists with >=1 provider-route, provider,
35
+ * evidence, decision, and artifact edge, cross-checks evidence counts/artifact
36
+ * counts/status against the manifest, and flags orphan runs and dangling edges.
37
+ */
38
+ export function auditGraphRuns(state, entries, statePath) {
39
+ const nodeIds = new Set(state.nodes.map((node) => node.id));
40
+ const runs = entries.map((entry) => auditSingleRun(state, nodeIds, entry.runId, entry.manifest ?? null));
41
+ const summary = { total: runs.length, passed: 0, partial: 0, failed: 0 };
42
+ for (const run of runs)
43
+ summary[run.verdict] += 1;
44
+ const verdict = summary.failed > 0 ? "failed" : summary.partial > 0 ? "partial" : "passed";
45
+ return {
46
+ schemaVersion: "omk.graph-audit.v1",
47
+ verdict,
48
+ statePath,
49
+ generatedAt: new Date().toISOString(),
50
+ summary,
51
+ runs,
52
+ };
53
+ }
54
+ function auditSingleRun(state, nodeIds, runId, manifest) {
55
+ const notes = [];
56
+ const mismatches = [];
57
+ const danglers = [];
58
+ const counts = { providerRoute: 0, provider: 0, evidence: 0, decision: 0, artifact: 0 };
59
+ const manifestPresent = manifest !== null;
60
+ const runNode = state.nodes.find((node) => node.type === "Run" && node.properties.runId === runId);
61
+ if (!runNode) {
62
+ notes.push("run node not found in graph");
63
+ if (!manifestPresent)
64
+ notes.push("run-manifest.json not found");
65
+ return { runId, runNodeFound: false, orphan: false, manifestPresent, counts, mismatches, danglers, verdict: "failed", notes };
66
+ }
67
+ const outgoing = state.edges.filter((edge) => edge.from === runNode.id);
68
+ const routeEdges = outgoing.filter((edge) => edge.type === "HAS_PROVIDER_ROUTE");
69
+ counts.providerRoute = routeEdges.length;
70
+ counts.evidence = outgoing.filter((edge) => edge.type === "HAS_EVIDENCE").length;
71
+ counts.decision = outgoing.filter((edge) => edge.type === "HAS_DECISION").length;
72
+ counts.artifact = outgoing.filter((edge) => edge.type === "TOUCHES_FILE").length;
73
+ const routeToEdges = routeEdges.flatMap((routeEdge) => state.edges.filter((edge) => edge.from === routeEdge.to && edge.type === "ROUTES_TO"));
74
+ counts.provider = routeToEdges.length;
75
+ const orphan = outgoing.length === 0;
76
+ // Dangler detection across the run subgraph (run edges + route ROUTES_TO).
77
+ for (const edge of [...outgoing, ...routeToEdges]) {
78
+ if (!nodeIds.has(edge.from))
79
+ danglers.push({ edgeId: edge.id, type: edge.type, from: edge.from, to: edge.to, missing: "from" });
80
+ if (!nodeIds.has(edge.to))
81
+ danglers.push({ edgeId: edge.id, type: edge.type, from: edge.from, to: edge.to, missing: "to" });
82
+ }
83
+ if (manifest) {
84
+ const evidenceNode = outgoing
85
+ .filter((edge) => edge.type === "HAS_EVIDENCE")
86
+ .map((edge) => state.nodes.find((node) => node.id === edge.to))
87
+ .find((node) => node !== undefined);
88
+ if (evidenceNode) {
89
+ for (const field of EVIDENCE_SUMMARY_FIELDS) {
90
+ const graphValue = Number(evidenceNode.properties[field] ?? Number.NaN);
91
+ const manifestValue = manifest.evidenceSummary[field];
92
+ if (graphValue !== manifestValue) {
93
+ mismatches.push({ field: `evidence.${field}`, graph: Number.isNaN(graphValue) ? null : graphValue, manifest: manifestValue });
94
+ }
95
+ }
96
+ }
97
+ else {
98
+ notes.push("evidence node missing for cross-check");
99
+ }
100
+ if (counts.artifact !== manifest.artifacts.length) {
101
+ mismatches.push({ field: "artifact.count", graph: counts.artifact, manifest: manifest.artifacts.length });
102
+ }
103
+ const graphStatus = String(runNode.properties.status ?? "");
104
+ if (graphStatus !== manifest.status) {
105
+ mismatches.push({ field: "run.status", graph: graphStatus, manifest: manifest.status });
106
+ }
107
+ }
108
+ else {
109
+ notes.push("run-manifest.json not found; structural audit only");
110
+ }
111
+ const hasAllEdges = counts.providerRoute >= 1 &&
112
+ counts.provider >= 1 &&
113
+ counts.evidence >= 1 &&
114
+ counts.decision >= 1 &&
115
+ counts.artifact >= 1;
116
+ let verdict;
117
+ if (orphan || !hasAllEdges)
118
+ verdict = "failed";
119
+ else if (danglers.length > 0 || mismatches.length > 0 || !manifestPresent)
120
+ verdict = "partial";
121
+ else
122
+ verdict = "passed";
123
+ return { runId, runNodeFound: true, orphan, manifestPresent, counts, mismatches, danglers, verdict, notes };
124
+ }
125
+ async function loadGraphStateForAudit(statePath) {
126
+ try {
127
+ const raw = await readFile(statePath, "utf-8");
128
+ const parsed = JSON.parse(raw);
129
+ if (Array.isArray(parsed.nodes) && Array.isArray(parsed.edges)) {
130
+ return { ...parsed, nodes: parsed.nodes, edges: parsed.edges };
131
+ }
132
+ }
133
+ catch {
134
+ // fall through to an empty state
135
+ }
136
+ return {
137
+ version: 1,
138
+ ontology: { version: "", classes: [], relationTypes: [], description: "" },
139
+ project: { key: "", name: "", root: "" },
140
+ updatedAt: new Date().toISOString(),
141
+ nodes: [],
142
+ edges: [],
143
+ };
144
+ }
145
+ async function loadRunManifestForAudit(runId, root) {
146
+ try {
147
+ const raw = await readFile(getRunArtifactPath(runId, "run-manifest.json", root), "utf-8");
148
+ const parsed = RunManifestSchema.safeParse(JSON.parse(raw));
149
+ return parsed.success ? parsed.data : null;
150
+ }
151
+ catch {
152
+ return null;
153
+ }
154
+ }
155
+ export async function graphAuditCommand(options = {}) {
156
+ const startedAt = Date.now();
157
+ const root = getProjectRoot();
158
+ const statePath = options.input ? resolve(root, options.input) : join(root, ".omk", "memory", "graph-state.json");
159
+ const state = await loadGraphStateForAudit(statePath);
160
+ const runIds = options.run ? [validateRunId(options.run)] : await listValidRunIds(root);
161
+ const entries = [];
162
+ for (const runId of runIds) {
163
+ entries.push({ runId, manifest: await loadRunManifestForAudit(runId, root) });
164
+ }
165
+ const report = auditGraphRuns(state, entries, statePath);
166
+ if (options.json) {
167
+ console.log(JSON.stringify(createOmkJsonEnvelope({
168
+ command: "graph",
169
+ status: report.verdict,
170
+ data: report,
171
+ durationMs: Date.now() - startedAt,
172
+ })));
173
+ return;
174
+ }
175
+ printGraphAuditReport(report);
176
+ if (report.verdict === "failed")
177
+ process.exitCode = 1;
178
+ }
179
+ function printGraphAuditReport(report) {
180
+ console.log(header("OMK Graph Audit"));
181
+ if (report.statePath)
182
+ console.log(label("State", report.statePath));
183
+ console.log(label("Runs", `${report.summary.total} (passed ${report.summary.passed}, partial ${report.summary.partial}, failed ${report.summary.failed})`));
184
+ for (const run of report.runs) {
185
+ const line = `${run.runId} — routes ${run.counts.providerRoute}/providers ${run.counts.provider}/evidence ${run.counts.evidence}/decisions ${run.counts.decision}/artifacts ${run.counts.artifact}`;
186
+ if (run.verdict === "passed")
187
+ console.log(status.ok(line));
188
+ else if (run.verdict === "partial")
189
+ console.log(status.warn(line));
190
+ else
191
+ console.log(status.fail(line));
192
+ for (const mismatch of run.mismatches) {
193
+ console.log(label(" mismatch", `${mismatch.field}: graph=${String(mismatch.graph)} manifest=${String(mismatch.manifest)}`));
194
+ }
195
+ for (const dangler of run.danglers) {
196
+ console.log(label(" dangler", `${dangler.type} missing ${dangler.missing} (${dangler.from} -> ${dangler.to})`));
197
+ }
198
+ for (const note of run.notes)
199
+ console.log(label(" note", note));
200
+ }
201
+ const verdictLine = `Verdict: ${report.verdict}`;
202
+ if (report.verdict === "passed")
203
+ console.log(status.ok(verdictLine));
204
+ else if (report.verdict === "partial")
205
+ console.log(status.warn(verdictLine));
206
+ else
207
+ console.log(status.fail(verdictLine));
208
+ }
@@ -3,6 +3,7 @@ interface MergeOptions {
3
3
  runId?: string;
4
4
  strategy?: string;
5
5
  dryRun?: boolean;
6
+ json?: boolean;
6
7
  }
7
8
  export declare function mergeCommand(options: MergeOptions): Promise<void>;
8
9
  export {};
@@ -9,7 +9,95 @@ import { runQualityGate } from "../mcp/quality-gate.js";
9
9
  import { readTextFile } from "../util/fs.js";
10
10
  import { getOmkResourceSettings } from "../util/resource-profile.js";
11
11
  import { defaultScopedRoleAgentFile, writeScopedAgentFile } from "../util/scoped-agent-file.js";
12
+ import { createOmkJsonEnvelope } from "../util/json-envelope.js";
13
+ import { emitJson } from "../util/cli-contract.js";
14
+ /**
15
+ * JSON path for `omk merge --json`.
16
+ * Read-only preview: resolves the run, collects worktree diffs (git diff +
17
+ * `git apply --check`) and selects a winner by strategy, but does NOT run the
18
+ * reviewer, tests, patch apply, or quality gate. Emits exactly one
19
+ * omk.contract.v1 envelope (no banner, no ANSI) and never calls process.exit.
20
+ */
21
+ async function emitMergeJson(options) {
22
+ const started = Date.now();
23
+ const root = getProjectRoot();
24
+ const strategy = (options.strategy ?? "first").trim().toLowerCase();
25
+ const dryRun = Boolean(options.dryRun);
26
+ const emitNotApplicable = (runId, code, message) => {
27
+ emitJson(createOmkJsonEnvelope({
28
+ command: "merge",
29
+ status: "not-applicable",
30
+ ok: false,
31
+ ...(runId ? { runId } : {}),
32
+ data: { runId, strategy, dryRun, merged: null, conflicts: [], applied: 0 },
33
+ warnings: [{ code, message, recoverable: true, severity: "warning" }],
34
+ durationMs: Date.now() - started,
35
+ }));
36
+ };
37
+ if (!(await isGitRepo())) {
38
+ emitNotApplicable(null, "INTERNAL_ERROR", "Not a git repository.");
39
+ return;
40
+ }
41
+ const runsDir = getOmkPath("runs");
42
+ if (!(await pathExists(runsDir))) {
43
+ emitNotApplicable(null, "RUN_ARTIFACT_MISSING", "No runs found.");
44
+ return;
45
+ }
46
+ let runId = options.run ?? options.runId ?? "latest";
47
+ if (runId === "latest") {
48
+ const entries = await readdir(runsDir, { withFileTypes: true });
49
+ const runs = entries.filter((e) => e.isDirectory()).sort().reverse();
50
+ if (runs.length === 0) {
51
+ emitNotApplicable(null, "RUN_ARTIFACT_MISSING", "No runs found.");
52
+ return;
53
+ }
54
+ runId = runs[0].name;
55
+ }
56
+ const worktreesDir = getOmkPath(`worktrees/${runId}`);
57
+ if (!(await pathExists(worktreesDir))) {
58
+ emitNotApplicable(runId, "RUN_ARTIFACT_MISSING", "No worktrees found for run.");
59
+ return;
60
+ }
61
+ const workerNames = await readdir(worktreesDir, { withFileTypes: true }).then((e) => e.filter((d) => d.isDirectory()).map((d) => d.name));
62
+ const currentBranch = await getCurrentBranch();
63
+ if (!currentBranch) {
64
+ emitNotApplicable(runId, "INTERNAL_ERROR", "Could not determine current branch.");
65
+ return;
66
+ }
67
+ const workers = [];
68
+ for (const name of workerNames) {
69
+ const wtPath = join(worktreesDir, name);
70
+ const diffResult = await runShell("git", ["-C", wtPath, "diff", currentBranch], { timeout: 15000 });
71
+ if (diffResult.failed || !diffResult.stdout.trim())
72
+ continue;
73
+ const diff = diffResult.stdout;
74
+ const diffLines = diff.split("\n").length;
75
+ const applyCheck = await runShell("git", ["apply", "--check"], { cwd: root, input: diff, timeout: 15000 });
76
+ workers.push({ name, path: wtPath, diff, diffLines, canApply: !applyCheck.failed });
77
+ }
78
+ const winner = selectWinner(workers, strategy);
79
+ const data = {
80
+ runId,
81
+ strategy,
82
+ dryRun,
83
+ merged: winner?.name ?? null,
84
+ conflicts: workers.filter((w) => !w.canApply).map((w) => w.name),
85
+ applied: 0,
86
+ };
87
+ emitJson(createOmkJsonEnvelope({
88
+ command: "merge",
89
+ status: workers.length === 0 ? "not-applicable" : "dry-run",
90
+ ok: workers.length > 0,
91
+ runId,
92
+ data,
93
+ durationMs: Date.now() - started,
94
+ }));
95
+ }
12
96
  export async function mergeCommand(options) {
97
+ if (options.json === true || process.argv.includes("--json")) {
98
+ await emitMergeJson(options);
99
+ return;
100
+ }
13
101
  const root = getProjectRoot();
14
102
  const strategy = (options.strategy ?? "first").trim().toLowerCase();
15
103
  const dryRun = Boolean(options.dryRun);
@@ -11,7 +11,7 @@ import { writeMemoryRecallSummary } from "../../util/chat-startup.js";
11
11
  import { normalizeProviderPolicy, parseProviderModelArg } from "../../providers/model-registry.js";
12
12
  import { parseExecutionPromptPolicy } from "../../util/execution-selection.js";
13
13
  import { analyzeUserIntent } from "../../goal/intake.js";
14
- import { buildIntentFrame } from "../../goal/intent-frame.js";
14
+ import { buildIntentFrame, buildIntentFrameWithOuroboros } from "../../goal/intent-frame.js";
15
15
  import { renderCapabilityRoutingArtifact } from "../../orchestration/capability-routing.js";
16
16
  import { ensureCompletionArtifactContract } from "../../orchestration/completion-artifacts.js";
17
17
  import { buildParallelRouteDecision, resolveParallelCommandExecutionDecision, createInteractiveRunState, createExecutableDagFromState } from "./orchestrator.js";
@@ -70,7 +70,7 @@ export async function parallelCommand(goal, options = {}) {
70
70
  const { loadSpecDag } = await import("../dag-from-spec.js");
71
71
  const specDag = await loadSpecDag(options.fromSpec, { parallel: true });
72
72
  effectiveGoal = goal ?? `spec: ${specDag.nodes[0]?.name ?? options.fromSpec}`;
73
- intentFrame = buildIntentFrame(effectiveGoal);
73
+ intentFrame = await buildIntentFrameWithOuroboros(effectiveGoal);
74
74
  specNodes = specDag.nodes.map((node) => {
75
75
  const { status: _status, retries: _retries, ...def } = node;
76
76
  void _status;
@@ -79,7 +79,7 @@ export async function parallelCommand(goal, options = {}) {
79
79
  });
80
80
  }
81
81
  else {
82
- intentFrame = intentFrame ?? buildIntentFrame(effectiveGoal);
82
+ intentFrame = intentFrame ?? (await buildIntentFrameWithOuroboros(effectiveGoal));
83
83
  }
84
84
  const resolvedIntent = options.intent ?? analyzeUserIntent(effectiveGoal);
85
85
  const executionDecision = options.executionDecision ?? await resolveParallelCommandExecutionDecision({
@@ -1,6 +1,7 @@
1
1
  import { checkDeepSeekBalance } from "../providers/deepseek/deepseek-balance.js";
2
2
  import { forceDisableDeepSeek, getDeepSeekProviderStatus, resolveDeepSeekApiKey, setDeepSeekApiKey, setDeepSeekEnabled, } from "../providers/deepseek/deepseek-config.js";
3
3
  import { normalizeProviderId, providerDoctorStatus, readProviderRegistry, setProviderDefaults, setProviderConfig, setProviderEnabled, } from "../providers/model-registry.js";
4
+ import { toProviderHealth } from "../providers/provider-health.js";
4
5
  import { maskSensitiveText } from "../util/secret-mask.js";
5
6
  import { status, label, header, style } from "../util/theme.js";
6
7
  const PROVIDER_PROFILES = [
@@ -95,7 +96,7 @@ export async function providerDoctorCommand(provider, options = {}) {
95
96
  const normalized = normalizeProviderId(target);
96
97
  const payload = await providerDoctorStatus(normalized === "auto" ? "kimi" : normalized);
97
98
  if (options.json) {
98
- console.log(JSON.stringify(payload, null, 2));
99
+ console.log(JSON.stringify({ ...payload, health: toProviderHealth(payload) }, null, 2));
99
100
  }
100
101
  else {
101
102
  console.log(header("Provider doctor"));
@@ -125,13 +126,14 @@ export async function providerDoctorCommand(provider, options = {}) {
125
126
  disableForRun: true,
126
127
  };
127
128
  if (options.json) {
128
- console.log(JSON.stringify({
129
+ const deepseekDoctor = {
129
130
  ...result,
130
131
  enabled: providerStatus.enabled,
131
132
  disabledBy: providerStatus.disabledBy,
132
133
  apiKeySet: providerStatus.apiKeySet,
133
134
  apiKeySource: providerStatus.apiKeySource,
134
- }, null, 2));
135
+ };
136
+ console.log(JSON.stringify({ ...deepseekDoctor, health: toProviderHealth(deepseekDoctor) }, null, 2));
135
137
  }
136
138
  else {
137
139
  console.log(header("Provider doctor"));
@@ -1,5 +1,6 @@
1
- import { starGitHubRepo, getStarPromptSummary } from "../util/first-run-star.js";
1
+ import { starGitHubRepo, getStarPromptSummary, openRepoInBrowser, parseGitHubRepoSlug } from "../util/first-run-star.js";
2
2
  import { OMK_REPO_URL } from "../util/version.js";
3
+ import { style } from "../util/theme.js";
3
4
  export async function starCommand(options = {}) {
4
5
  if (options.status) {
5
6
  const summary = await getStarPromptSummary();
@@ -25,6 +26,10 @@ export async function starCommand(options = {}) {
25
26
  }
26
27
  catch (e) {
27
28
  console.error("Failed to star:", e instanceof Error ? e.message : String(e));
29
+ const slug = parseGitHubRepoSlug(OMK_REPO_URL);
30
+ if (slug)
31
+ console.error(style.gray(`Visit ${style.cream(`https://github.com/${slug}`)} to star manually.`));
32
+ await openRepoInBrowser(OMK_REPO_URL);
28
33
  process.exit(1);
29
34
  }
30
35
  }
@@ -1,2 +1,5 @@
1
- export declare function summaryLatestCommand(): Promise<void>;
1
+ export interface SummaryLatestCommandOptions {
2
+ json?: boolean;
3
+ }
4
+ export declare function summaryLatestCommand(options?: SummaryLatestCommandOptions): Promise<void>;
2
5
  export declare function summaryShowCommand(runId?: string): Promise<void>;
@@ -2,6 +2,8 @@ import { readFile, writeFile, readdir, stat } from "fs/promises";
2
2
  import { join } from "path";
3
3
  import { getProjectRoot, pathExists, getRunsDir, getRunPath } from "../util/fs.js";
4
4
  import { style, header, status, label } from "../util/theme.js";
5
+ import { createOmkJsonEnvelope } from "../util/json-envelope.js";
6
+ import { emitJson } from "../util/cli-contract.js";
5
7
  function computeProviderRouting(nodes) {
6
8
  const byProvider = {};
7
9
  const fallbacks = [];
@@ -257,7 +259,107 @@ function generateReportMd(s) {
257
259
  lines.push("");
258
260
  return lines.join("\n");
259
261
  }
260
- export async function summaryLatestCommand() {
262
+ function deriveRunRollupStatus(s) {
263
+ if (s.failed > 0)
264
+ return "failed";
265
+ if (s.blocked > 0)
266
+ return "blocked";
267
+ if (s.running > 0 || s.pending > 0)
268
+ return "running";
269
+ if (s.totalNodes > 0 && s.done === s.totalNodes)
270
+ return "passed";
271
+ return "partial";
272
+ }
273
+ function buildSummaryJsonData(summary) {
274
+ const data = {
275
+ runId: summary.runId,
276
+ status: deriveRunRollupStatus(summary),
277
+ nodes: { total: summary.totalNodes, passed: summary.done, failed: summary.failed },
278
+ successRate: summary.successRate,
279
+ durationMs: summary.durationMs,
280
+ startedAt: summary.startedAt,
281
+ };
282
+ if (summary.completedAt)
283
+ data.completedAt = summary.completedAt;
284
+ if (summary.providerRouting.attempts > 0) {
285
+ data.providerRoute = {
286
+ attempts: summary.providerRouting.attempts,
287
+ byProvider: summary.providerRouting.byProvider,
288
+ fallbacks: summary.providerRouting.fallbacks.length,
289
+ };
290
+ }
291
+ const evidenceRefs = summary.nodes.flatMap((node) => (node.evidence ?? []).map((e) => ({ nodeId: node.id, gate: e.gate, passed: e.passed })));
292
+ if (evidenceRefs.length > 0)
293
+ data.evidenceRefs = evidenceRefs;
294
+ return data;
295
+ }
296
+ function emptySummaryJsonData(runId) {
297
+ return {
298
+ runId,
299
+ status: "no-runs",
300
+ nodes: { total: 0, passed: 0, failed: 0 },
301
+ successRate: 0,
302
+ durationMs: 0,
303
+ };
304
+ }
305
+ /**
306
+ * Read-only JSON path for `omk summary --json`.
307
+ * Emits EXACTLY ONE omk.contract.v1 envelope to stdout (no banner, no ANSI),
308
+ * never calls process.exit, and never writes summary.md/report.md side effects.
309
+ */
310
+ async function emitSummaryJson() {
311
+ const started = Date.now();
312
+ const root = getProjectRoot();
313
+ const latestId = await findLatestRunId(root);
314
+ if (!latestId) {
315
+ emitJson(createOmkJsonEnvelope({
316
+ command: "summary",
317
+ status: "not-applicable",
318
+ ok: false,
319
+ data: emptySummaryJsonData(null),
320
+ warnings: [
321
+ { code: "RUN_ARTIFACT_MISSING", message: "No runs found.", recoverable: true, severity: "warning" },
322
+ ],
323
+ durationMs: Date.now() - started,
324
+ }));
325
+ return;
326
+ }
327
+ const state = await loadRunState(root, latestId);
328
+ if (!state) {
329
+ emitJson(createOmkJsonEnvelope({
330
+ command: "summary",
331
+ status: "not-applicable",
332
+ ok: false,
333
+ runId: latestId,
334
+ data: emptySummaryJsonData(latestId),
335
+ warnings: [
336
+ {
337
+ code: "RUN_ARTIFACT_MISSING",
338
+ message: `Run state not found for ${latestId}`,
339
+ recoverable: true,
340
+ severity: "warning",
341
+ },
342
+ ],
343
+ durationMs: Date.now() - started,
344
+ }));
345
+ return;
346
+ }
347
+ const summary = computeSummary(state);
348
+ emitJson(createOmkJsonEnvelope({
349
+ command: "summary",
350
+ status: "passed",
351
+ ok: true,
352
+ runId: summary.runId,
353
+ data: buildSummaryJsonData(summary),
354
+ durationMs: Date.now() - started,
355
+ }));
356
+ }
357
+ export async function summaryLatestCommand(options = {}) {
358
+ const jsonMode = options.json === true || process.argv.includes("--json");
359
+ if (jsonMode) {
360
+ await emitSummaryJson();
361
+ return;
362
+ }
261
363
  const root = await getProjectRoot();
262
364
  const latestId = await findLatestRunId(root);
263
365
  if (!latestId) {
@@ -2,6 +2,7 @@ import type { TeamRuntimeStatus } from "../contracts/orchestration.js";
2
2
  export declare function teamCommand(options?: {
3
3
  workers?: string;
4
4
  runId?: string;
5
+ json?: boolean;
5
6
  }): Promise<void>;
6
7
  export declare function writeTeamRunState(root: string, runId: string, workerCount: number, session?: string, profile?: string): Promise<string>;
7
8
  export declare function buildExpectedTeamRuntimeStatus(session: string, statePath: string, workerCount: number, runtimeStatus: TeamRuntimeStatus["status"], presentWindows?: Map<string, number>): TeamRuntimeStatus;