open-multi-agent-kit 0.79.3 → 0.80.2

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 (128) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/MATURITY.md +2 -2
  3. package/README.md +86 -39
  4. package/ROADMAP.md +20 -17
  5. package/dist/brand/night-city.theme.json +122 -24
  6. package/dist/brand/rust-forge.theme.json +1 -1
  7. package/dist/commands/chat/native-root-loop.d.ts +27 -3
  8. package/dist/commands/chat/native-root-loop.js +370 -49
  9. package/dist/commands/dag.js +45 -0
  10. package/dist/commands/init/content.d.ts +1 -1
  11. package/dist/commands/init/content.js +3 -3
  12. package/dist/commands/init.js +3 -3
  13. package/dist/commands/verify.js +59 -1
  14. package/dist/contracts/dag.d.ts +9 -1
  15. package/dist/evidence/graph-ontology-relation-gate.d.ts +10 -0
  16. package/dist/evidence/graph-ontology-relation-gate.js +46 -0
  17. package/dist/evidence/headroom-replay-verifier.d.ts +19 -0
  18. package/dist/evidence/headroom-replay-verifier.js +86 -0
  19. package/dist/memory/local-graph-memory-store.d.ts +75 -0
  20. package/dist/memory/local-graph-memory-store.js +570 -0
  21. package/dist/orchestration/dag.d.ts +22 -1
  22. package/dist/orchestration/evidence-gate.js +12 -0
  23. package/dist/orchestration/executor.js +111 -15
  24. package/dist/orchestration/loop-controller-headroom.d.ts +6 -0
  25. package/dist/orchestration/loop-controller-headroom.js +33 -0
  26. package/dist/orchestration/scheduler.d.ts +2 -0
  27. package/dist/orchestration/scheduler.js +3 -0
  28. package/dist/orchestration/task-graph.d.ts +17 -0
  29. package/dist/orchestration/task-graph.js +38 -0
  30. package/dist/providers/codex-cli-runner.js +17 -2
  31. package/dist/runtime/agent-runtime.d.ts +32 -3
  32. package/dist/runtime/agent-runtime.js +20 -7
  33. package/dist/runtime/authority-matrix.d.ts +23 -0
  34. package/dist/runtime/authority-matrix.js +171 -0
  35. package/dist/runtime/chat-advisory-runtime.js +3 -0
  36. package/dist/runtime/codex-cli-runtime.js +2 -0
  37. package/dist/runtime/codex-runtime.d.ts +4 -1
  38. package/dist/runtime/codex-runtime.js +34 -12
  39. package/dist/runtime/context-broker-converter.js +32 -2
  40. package/dist/runtime/context-broker.js +18 -0
  41. package/dist/runtime/contracts/evidence.d.ts +52 -0
  42. package/dist/runtime/contracts/evidence.js +138 -1
  43. package/dist/runtime/contracts/shared.d.ts +27 -0
  44. package/dist/runtime/data-retention-gate.d.ts +42 -0
  45. package/dist/runtime/data-retention-gate.js +123 -0
  46. package/dist/runtime/deepseek-runtime.d.ts +5 -1
  47. package/dist/runtime/deepseek-runtime.js +13 -7
  48. package/dist/runtime/external-cli-adapter.js +20 -7
  49. package/dist/runtime/freedomd-evidence-envelope.d.ts +57 -0
  50. package/dist/runtime/freedomd-evidence-envelope.js +110 -0
  51. package/dist/runtime/freedomd-exception.d.ts +32 -0
  52. package/dist/runtime/freedomd-exception.js +95 -0
  53. package/dist/runtime/freedomd-incidents.d.ts +23 -0
  54. package/dist/runtime/freedomd-incidents.js +103 -0
  55. package/dist/runtime/freedomd-policy.d.ts +35 -0
  56. package/dist/runtime/freedomd-policy.js +114 -0
  57. package/dist/runtime/freedomd-router.d.ts +66 -0
  58. package/dist/runtime/freedomd-router.js +433 -0
  59. package/dist/runtime/headroom-aware-loop-decision.d.ts +21 -0
  60. package/dist/runtime/headroom-aware-loop-decision.js +67 -0
  61. package/dist/runtime/headroom-loop-risk.d.ts +19 -0
  62. package/dist/runtime/headroom-loop-risk.js +43 -0
  63. package/dist/runtime/headroom-policy.d.ts +14 -1
  64. package/dist/runtime/headroom-policy.js +74 -12
  65. package/dist/runtime/kimi-api-runtime.d.ts +4 -1
  66. package/dist/runtime/kimi-api-runtime.js +35 -11
  67. package/dist/runtime/kimi-print-runtime.js +21 -0
  68. package/dist/runtime/kimi-wire-protocol-runtime.js +17 -1
  69. package/dist/runtime/local-llm-runtime.d.ts +5 -1
  70. package/dist/runtime/local-llm-runtime.js +14 -23
  71. package/dist/runtime/private-stderr.d.ts +13 -0
  72. package/dist/runtime/private-stderr.js +69 -0
  73. package/dist/runtime/prompt-envelope.d.ts +1 -1
  74. package/dist/runtime/provider-sovereignty.d.ts +81 -0
  75. package/dist/runtime/provider-sovereignty.js +285 -0
  76. package/dist/runtime/runtime-backed-task-runner.d.ts +1 -0
  77. package/dist/runtime/runtime-backed-task-runner.js +282 -10
  78. package/dist/runtime/runtime-bootstrap.d.ts +12 -0
  79. package/dist/runtime/runtime-bootstrap.js +54 -11
  80. package/dist/runtime/runtime-failure-classifier.d.ts +15 -0
  81. package/dist/runtime/runtime-failure-classifier.js +107 -0
  82. package/dist/runtime/runtime-health-probes.d.ts +30 -0
  83. package/dist/runtime/runtime-health-probes.js +102 -0
  84. package/dist/runtime/runtime-router.d.ts +6 -2
  85. package/dist/runtime/runtime-router.js +575 -53
  86. package/dist/runtime/structured-compaction.d.ts +107 -0
  87. package/dist/runtime/structured-compaction.js +414 -0
  88. package/dist/runtime/tool-dispatch-contracts.d.ts +7 -5
  89. package/dist/runtime/tool-dispatch-contracts.js +15 -4
  90. package/dist/runtime/tool-plane.d.ts +1 -1
  91. package/dist/runtime/tool-plane.js +49 -1
  92. package/dist/runtime/worker-manifest.js +1 -1
  93. package/dist/schema/evidence-bundle.schema.d.ts +8 -8
  94. package/dist/schema/proof-bundle.schema.d.ts +2 -2
  95. package/dist/util/hash.d.ts +2 -0
  96. package/dist/util/hash.js +13 -0
  97. package/dist/util/session.js +5 -4
  98. package/docs/2026-06-16/critical-issues.md +19 -0
  99. package/docs/2026-06-16/improvements.md +15 -0
  100. package/docs/2026-06-16/init-checklist.md +25 -0
  101. package/docs/2026-06-16/plan.md +20 -0
  102. package/docs/2026-06-17/critical-issues.md +19 -0
  103. package/docs/2026-06-17/improvements.md +15 -0
  104. package/docs/2026-06-17/init-checklist.md +25 -0
  105. package/docs/2026-06-17/plan.md +20 -0
  106. package/docs/ARCHITECTURE_ANALYSIS_CODEGRAPH.md +2 -1
  107. package/docs/GSTACK_MIGRATION.md +1 -1
  108. package/docs/algorithm-hardening-playbook.md +219 -0
  109. package/docs/claims.md +1 -1
  110. package/docs/getting-started.md +1 -1
  111. package/docs/native-root-runtime-hardening.md +42 -32
  112. package/docs/post-0793-hardening-roadmap.md +123 -0
  113. package/docs/provider-maturity.md +18 -16
  114. package/docs/versioning.md +3 -3
  115. package/docs/what-is-omk.md +1 -1
  116. package/package.json +8 -4
  117. package/readmeasset/.npmignore +2 -0
  118. package/readmeasset/ASSET_INDEX.md +2 -0
  119. package/readmeasset/ASSET_PROVENANCE.md +40 -20
  120. package/readmeasset/omk-adaptorch-ouroboros-supermemory.svg +1 -1
  121. package/readmeasset/omk-control-surfaces.svg +1 -1
  122. package/readmeasset/omk-core-loop.svg +1 -1
  123. package/readmeasset/omk-freedomd-control-plane.svg +59 -0
  124. package/readmeasset/omk-freedomd-control-plane.webp +0 -0
  125. package/readmeasset/omk-init-control-loop.svg +1 -1
  126. package/readmeasset/omk-logo-mark.svg +1 -1
  127. package/readmeasset/omk-parallel-subagents.svg +1 -1
  128. package/readmeasset/omk-release-assertions.svg +1 -1
@@ -1,10 +1,15 @@
1
1
  import { style } from "../../util/theme.js";
2
+ import { getRunArtifactPath } from "../../util/run-store.js";
2
3
  import { runShell } from "../../util/shell.js";
4
+ import { mkdir, writeFile, appendFile, readFile } from "node:fs/promises";
5
+ import { mkdirSync, writeFileSync } from "node:fs";
6
+ import { createHash } from "node:crypto";
7
+ import { dirname } from "node:path";
3
8
  import { createDag } from "../../orchestration/dag.js";
4
9
  import { applyCapabilityInjectionToRouting, buildCapabilityInjection, } from "../../runtime/capability-injection.js";
5
10
  import { capabilityScopesFromRouting } from "../../orchestration/capability-routing.js";
6
11
  import { decideToolAuthority, } from "../../safety/tool-authority-gate.js";
7
- import { resolveToolAuthorityEnforcement } from "../../runtime/tool-dispatch-contracts.js";
12
+ import { resolveToolAuthorityMode } from "../../runtime/tool-dispatch-contracts.js";
8
13
  import { compileBloatToNlp, } from "../../runtime/debloat-nlp.js";
9
14
  import { buildPromptEnvelope, renderPromptEnvelope, } from "../../runtime/prompt-envelope.js";
10
15
  import { buildTaskRunContext } from "../../runtime/worker-manifest.js";
@@ -12,6 +17,7 @@ import { buildInputEnvelope, normalizeMcpScope, } from "../../input/input-envelo
12
17
  import { persistInputEnvelope } from "../../input/input-artifacts.js";
13
18
  import { buildDagCompileResult } from "../../orchestration/dag-compiler.js";
14
19
  import { persistDagCompileArtifacts } from "../../orchestration/dag-artifacts.js";
20
+ import { evidenceObservationsFromResult, evidenceRequirementsFromOutputs } from "../../runtime/contracts/evidence.js";
15
21
  import { TerminalOwner } from "../../util/terminal-owner.js";
16
22
  import { resumeInteractiveInput } from "../../util/terminal-input.js";
17
23
  import { executeHarnessRun } from "../../harness/execute-harness-run.js";
@@ -25,6 +31,8 @@ import { buildNativeChatSlashCommands } from "./slash/commands/index.js";
25
31
  import { parseSlashInput } from "./slash/parser.js";
26
32
  import { createSlashCommandRegistry } from "./slash/registry.js";
27
33
  import { emitSlashResult, okSlashResult, printSlashResult, } from "./slash/result.js";
34
+ import { LocalGraphMemoryStore } from "../../memory/local-graph-memory-store.js";
35
+ import { runtimeIsAdvisory } from "../../runtime/authority-matrix.js";
28
36
  async function runSlashHandler(handler, parsed, ctx, options = {}) {
29
37
  const result = await handler.handler(ctx, parsed.args);
30
38
  const normalized = result ?? okSlashResult();
@@ -317,22 +325,15 @@ function nativeTurnRiskToToolOp(risk) {
317
325
  }
318
326
  }
319
327
  /**
320
- * Shadow-only tool-authority observability at the live turn dispatch
321
- * checkpoint. The kimi runner executes tools inside a spawned CLI, so per-tool
322
- * enforcement lives in dispatchToolCallsByContract; here we only compute and
323
- * record the turn-level verdict for the trace. Default output is byte-identical
324
- * (emitted only under OMK_DEBUG / OMK_TOOL_AUTHORITY_TRACE); this path never
325
- * blocks dispatch.
328
+ * Turn-level tool-authority checkpoint. Shadow mode records only when tracing is
329
+ * enabled, warn mode emits a diagnostic, and enforce mode fail-closes block/ask
330
+ * verdicts before dispatching the native turn.
326
331
  */
327
332
  function recordNativeTurnToolAuthority(input) {
328
- const traceEnabled = input.env.OMK_DEBUG === "1" ||
329
- /^(1|true|yes|on)$/i.test(input.env.OMK_TOOL_AUTHORITY_TRACE ?? "");
330
- if (!traceEnabled)
331
- return;
332
333
  const routing = input.node.routing;
333
334
  const scopes = capabilityScopesFromRouting(routing);
334
335
  const op = nativeTurnRiskToToolOp(routing?.risk);
335
- const enforce = resolveToolAuthorityEnforcement(input.env);
336
+ const mode = resolveToolAuthorityMode(input.env);
336
337
  const decision = decideToolAuthority({
337
338
  op,
338
339
  writeAuthority: scopes.writeAuthority,
@@ -341,15 +342,23 @@ function recordNativeTurnToolAuthority(input) {
341
342
  sandboxMode: routing?.sandboxMode === "read-only" ? "read-only" : "workspace-write",
342
343
  tty: Boolean(process.stdout.isTTY),
343
344
  });
344
- const line = ` tool-authority(${enforce ? "enforce" : "shadow"}): node=${input.node.id} ` +
345
- `op=${op} decision=${decision} write=${scopes.writeAuthority} ` +
346
- `shell=${scopes.shellAuthority} sandbox=${routing?.sandboxMode ?? "auto"}\n`;
347
- if (input.renderer) {
348
- input.renderer.emit({ type: "control:output", text: style.phosphorDim(line) });
349
- }
350
- else {
351
- process.stderr.write(style.phosphorDim(line));
345
+ const enforced = mode === "enforce" && decision !== "allow";
346
+ const reason = `tool-authority ${decision} for ${op} op ` +
347
+ `(write=${scopes.writeAuthority}, shell=${scopes.shellAuthority}, ` +
348
+ `sandbox=${routing?.sandboxMode ?? "auto"}, mode=${mode})`;
349
+ const traceEnabled = mode !== "shadow" ||
350
+ input.env.OMK_DEBUG === "1" ||
351
+ /^(1|true|yes|on)$/i.test(input.env.OMK_TOOL_AUTHORITY_TRACE ?? "");
352
+ if (traceEnabled) {
353
+ const line = ` ${reason}${enforced ? " [enforced]" : ""}\n`;
354
+ if (input.renderer) {
355
+ input.renderer.emit({ type: "control:output", text: style.phosphorDim(line) });
356
+ }
357
+ else {
358
+ process.stderr.write(style.phosphorDim(line));
359
+ }
352
360
  }
361
+ return { mode, op, decision, enforced, reason };
353
362
  }
354
363
  function emitNativeTurnRoute(input) {
355
364
  if (!input.heartbeatEnabled)
@@ -449,30 +458,96 @@ async function persistNativeDagCompileArtifacts(input) {
449
458
  process.stderr.write(style.metricsRed(text) + "\n");
450
459
  }
451
460
  }
452
- const API_ADVISORY_PROVIDERS = new Set([
453
- "deepseek",
454
- "kimi",
455
- "local-llm",
456
- "mimo",
457
- "openrouter",
458
- "qwen",
459
- ]);
460
- function hasExplicitReadOnlyIntent(text) {
461
+ function hasExplicitReadOnlyConstraint(text) {
462
+ return (/\b(without changing|without editing|do not change|don't change|do not edit|don't edit|no edits?|no file changes?|read[- ]only)\b/.test(text) ||
463
+ /읽기\s*전용|수정하지\s*말|변경하지\s*말|파일\s*(수정|변경)\s*(없이|하지\s*말)|검토만|분석만/.test(text));
464
+ }
465
+ function hasReadOnlyIntent(text) {
461
466
  return (/\b(read|inspect|look|show|list|summarize|explain|describe|review|audit|status|diagnose)\b/.test(text) ||
462
- /\b(without changing|without editing|do not change|don't change|do not edit|don't edit|no edits?|no file changes?)\b/.test(text) ||
463
- /읽기\s*전용|수정하지\s*말|변경하지\s*말|파일\s*(수정|변경)\s*(없이|하지\s*말)|요약|설명|상태|검토만|분석만|읽어|살펴/.test(text));
467
+ /요약|설명|상태|검토|분석|읽어|살펴/.test(text));
468
+ }
469
+ function detectExcludedOps(text) {
470
+ const excluded = new Set();
471
+ const scopes = [];
472
+ const add = (op, scope) => {
473
+ excluded.add(op);
474
+ scopes.push(scope);
475
+ };
476
+ const mergeTerms = "push|publish|release|merge|tag|deploy|version|npm publish|릴리즈|머지|배포|태그|푸시|버전|퍼블리시";
477
+ const shellTerms = "run|test|build|exec|execute|shell|terminal|command|npm|pnpm|yarn|bun|pytest|cargo|tsc|lint|verify|check|테스트|빌드|실행|검증|쉘|터미널|체크|명령";
478
+ const writeTerms = "fix|edit|write|implement|modify|patch|refactor|add|create|delete|update|change|수정|구현|패치|리팩터|추가|삭제|변경";
479
+ const englishNegationPrefix = "no|not|without|excluding|exclude|except|skip|skipping|omit|avoid|but not|do not|don't";
480
+ const englishNegationSuffix = "excluded|excepted|skipped|omitted|disabled|only on explicit request|only when requested";
481
+ if (new RegExp(`\\b(?:${englishNegationPrefix})\\b.{0,40}\\b(?:${mergeTerms})\\b`, "i").test(text) ||
482
+ new RegExp(`\\b(?:${mergeTerms})\\b.{0,40}\\b(?:${englishNegationSuffix})\\b`, "i").test(text) ||
483
+ /(?:릴리즈|머지|배포|태그|푸시|버전|퍼블리시).{0,16}(?:빼고|제외|없이|말고|하지\s*말|금지|명령시(?:에)?만|요청시(?:에)?만)/.test(text) ||
484
+ /(?:빼고|제외|없이|말고).{0,16}(?:릴리즈|머지|배포|태그|푸시|버전|퍼블리시)/.test(text)) {
485
+ add("merge", "excluded-merge-scope");
486
+ }
487
+ if (new RegExp(`\\b(?:${englishNegationPrefix})\\b.{0,40}\\b(?:${shellTerms})\\b`, "i").test(text) ||
488
+ new RegExp(`\\b(?:${shellTerms})\\b.{0,40}\\b(?:${englishNegationSuffix})\\b`, "i").test(text) ||
489
+ /(?:npm|테스트|빌드|실행|검증|체크|명령|쉘|터미널).{0,16}(?:빼고|제외|없이|말고|하지\s*말|금지|명령시(?:에)?만|요청시(?:에)?만)/.test(text) ||
490
+ /(?:빼고|제외|없이|말고).{0,16}(?:npm|테스트|빌드|실행|검증|체크|명령|쉘|터미널)/.test(text)) {
491
+ add("shell", "excluded-shell-scope");
492
+ }
493
+ if (new RegExp(`\\b(?:${englishNegationPrefix})\\b.{0,40}\\b(?:${writeTerms})\\b`, "i").test(text) ||
494
+ /(?:수정|구현|패치|리팩터|추가|삭제|변경).{0,16}(?:빼고|제외|없이|말고|하지\s*말|금지)/.test(text)) {
495
+ add("write", "excluded-write-scope");
496
+ }
497
+ return { ops: [...excluded], scopes };
498
+ }
499
+ function nativeRiskTrace(input) {
500
+ return {
501
+ risk: input.risk,
502
+ confidence: input.confidence,
503
+ matchedSignal: input.matchedSignal,
504
+ readOnlyOverride: input.readOnlyOverride ?? false,
505
+ positiveSignals: [...(input.positiveSignals ?? [])],
506
+ negativeScopes: [...(input.negativeScopes ?? [])],
507
+ requestedOps: [...(input.requestedOps ?? [])],
508
+ excludedOps: [...(input.excludedOps ?? [])],
509
+ };
464
510
  }
465
511
  export function inferNativeTurnRisk(prompt) {
466
512
  const text = prompt.toLowerCase();
467
- if (/\b(push|publish|release|merge|tag|deploy)\b|푸시|퍼블리시|릴리즈|머지|배포/.test(text))
468
- return "merge";
469
- if (/\b(run|test|build|exec|execute|shell|terminal|command|npm|pnpm|yarn|bun|pytest|cargo|go test|tsc|lint|verify|check)\b|테스트|빌드|실행|검증|쉘|터미널/.test(text))
470
- return "shell";
471
- if (/\b(fix|edit|write|implement|modify|patch|refactor|add|create|delete|update|change)\b|수정|구현|패치|리팩터|추가|삭제|변경/.test(text))
472
- return "write";
473
- if (hasExplicitReadOnlyIntent(text))
474
- return "read";
475
- return "write";
513
+ const excluded = detectExcludedOps(text);
514
+ if (hasExplicitReadOnlyConstraint(text)) {
515
+ return nativeRiskTrace({ risk: "read", confidence: 0.95, matchedSignal: "explicit-read-only-constraint", readOnlyOverride: true, negativeScopes: excluded.scopes, excludedOps: excluded.ops });
516
+ }
517
+ const candidates = [
518
+ { risk: "merge", confidence: 0.9, signal: "merge/release-keyword", pattern: /\b(push|publish|release|merge|tag|deploy)\b|푸시|퍼블리시|릴리즈|머지|배포/ },
519
+ { risk: "shell", confidence: 0.85, signal: "shell/execution-keyword", pattern: /\b(run|test|build|exec|execute|shell|terminal|command|npm|pnpm|yarn|bun|pytest|cargo|go test|tsc|lint|verify|check)\b|테스트|빌드|실행|검증|쉘|터미널/ },
520
+ { risk: "write", confidence: 0.85, signal: "write/modify-keyword", pattern: /\b(fix|edit|write|implement|modify|patch|refactor|add|create|delete|update|change)\b|수정|구현|패치|리팩터|추가|삭제|변경/ },
521
+ ];
522
+ const positive = candidates.filter((candidate) => candidate.pattern.test(text));
523
+ const requestedOps = positive.map((candidate) => candidate.risk);
524
+ const selected = positive.find((candidate) => !excluded.ops.includes(candidate.risk));
525
+ if (selected) {
526
+ return nativeRiskTrace({
527
+ risk: selected.risk,
528
+ confidence: selected.confidence,
529
+ matchedSignal: selected.signal,
530
+ positiveSignals: positive.map((candidate) => candidate.signal),
531
+ negativeScopes: excluded.scopes,
532
+ requestedOps,
533
+ excludedOps: excluded.ops,
534
+ });
535
+ }
536
+ if (positive.length > 0) {
537
+ const risk = hasReadOnlyIntent(text) ? "read" : "ask";
538
+ return nativeRiskTrace({
539
+ risk,
540
+ confidence: risk === "read" ? 0.75 : 0.55,
541
+ matchedSignal: `all-positive-risk-signals-excluded:${positive.map((candidate) => candidate.signal).join(",")}`,
542
+ positiveSignals: positive.map((candidate) => candidate.signal),
543
+ negativeScopes: excluded.scopes,
544
+ requestedOps,
545
+ excludedOps: excluded.ops,
546
+ });
547
+ }
548
+ if (hasReadOnlyIntent(text))
549
+ return nativeRiskTrace({ risk: "read", confidence: 0.75, matchedSignal: "read-only-intent", negativeScopes: excluded.scopes, excludedOps: excluded.ops });
550
+ return nativeRiskTrace({ risk: "ask", confidence: 0.5, matchedSignal: "no-clear-signal", negativeScopes: excluded.scopes, excludedOps: excluded.ops });
476
551
  }
477
552
  function nativeTurnRoutingPolicy(provider, risk) {
478
553
  if (isApiAdvisoryProvider(provider) && risk !== "read") {
@@ -483,7 +558,7 @@ function nativeTurnRoutingPolicy(provider, risk) {
483
558
  providerReasonSuffix: `; ${provider} API is advisory/read-only for ${risk} intent; OMK owns write, shell, MCP, and merge authority`,
484
559
  };
485
560
  }
486
- if (risk === "read") {
561
+ if (risk === "read" || risk === "ask") {
487
562
  return { capabilities: ["read"], readOnly: true, sandboxMode: "read-only" };
488
563
  }
489
564
  if (risk === "write") {
@@ -507,18 +582,79 @@ function nativeTurnRoutingPolicy(provider, risk) {
507
582
  };
508
583
  }
509
584
  function isApiAdvisoryProvider(provider) {
510
- return API_ADVISORY_PROVIDERS.has(provider);
585
+ return runtimeIsAdvisory({
586
+ id: `${provider}-api`,
587
+ providerId: provider,
588
+ runtimeMode: "api",
589
+ kind: "api",
590
+ });
591
+ }
592
+ function hashPrompt(value) {
593
+ return createHash("sha256").update(value).digest("hex");
594
+ }
595
+ function redactedPromptLabel(prompt) {
596
+ const compact = prompt.replace(/\s+/g, " ").trim().replace(/(sk-[A-Za-z0-9_-]+|ghp_[A-Za-z0-9_]+|[A-Za-z0-9+/]{32,}=*)/g, "[redacted]");
597
+ const summary = compact.length > 80 ? `${compact.slice(0, 77)}...` : compact;
598
+ return summary ? `native turn: ${summary}` : "native turn";
599
+ }
600
+ function writePrivatePromptArtifact(input) {
601
+ try {
602
+ const rel = `private/prompts/${sanitizeRunIdForPath(input.nodeId)}.json`;
603
+ const artifact = getRunArtifactPath(input.runId, rel);
604
+ mkdirSync(dirname(artifact), { recursive: true });
605
+ writeFileSync(artifact, JSON.stringify({
606
+ schemaVersion: "omk.prompt-envelope.v1",
607
+ nodeId: input.nodeId,
608
+ runId: input.runId,
609
+ promptHash: input.promptHash,
610
+ userPrompt: input.userPrompt,
611
+ compiledPrompt: input.compiledPrompt,
612
+ redaction: "private-run-artifact",
613
+ timestamp: new Date().toISOString(),
614
+ }, null, 2), "utf-8");
615
+ return rel;
616
+ }
617
+ catch {
618
+ return undefined;
619
+ }
511
620
  }
512
621
  function debloatRiskFromNativeTurnRisk(risk) {
513
- if (risk === "read")
622
+ if (risk === "read" || risk === "ask")
514
623
  return "read";
515
624
  if (risk === "write")
516
625
  return "write";
517
626
  return "dangerous";
518
627
  }
628
+ function isStrictGuardrailMode() {
629
+ const raw = process.env.OMK_STRICT_GUARDRAIL ?? "";
630
+ const normalized = raw.trim().toLowerCase();
631
+ return normalized === "1" || normalized === "true" || normalized === "on";
632
+ }
633
+ function nativeTurnRequiresEvidence(risk) {
634
+ if (isStrictGuardrailMode())
635
+ return risk === "write" || risk === "shell" || risk === "merge";
636
+ return false;
637
+ }
638
+ function nativeEvidenceOutputForRisk(risk) {
639
+ if (!nativeTurnRequiresEvidence(risk))
640
+ return undefined;
641
+ if (risk === "shell") {
642
+ return [{ name: "command evidence", gate: "command-pass", required: true }];
643
+ }
644
+ if (risk === "merge") {
645
+ return [{ name: "release evidence", ref: "## Evidence", gate: "summary", required: true }];
646
+ }
647
+ return [{ name: "change evidence", ref: "## Evidence", gate: "summary", required: true }];
648
+ }
519
649
  export function buildNativeRootLoopTurnNode(input) {
520
650
  const id = input.nodeId ?? `turn-${Date.now()}`;
521
- const turnRisk = inferNativeTurnRisk(input.prompt);
651
+ const riskTrace = inferNativeTurnRisk(input.prompt);
652
+ let turnRisk = riskTrace.risk;
653
+ // Low-confidence prompts fall back to ask/read-only to preserve safety.
654
+ if (riskTrace.confidence < 0.6 && turnRisk !== "read") {
655
+ turnRisk = "ask";
656
+ }
657
+ const evidenceRequired = nativeTurnRequiresEvidence(turnRisk);
522
658
  const routingPolicy = nativeTurnRoutingPolicy(input.bootstrap.provider, turnRisk);
523
659
  const capabilityInjection = buildCapabilityInjection({
524
660
  mcpAllowlist: input.mcpAllowlist,
@@ -545,7 +681,7 @@ export function buildNativeRootLoopTurnNode(input) {
545
681
  sandbox: routingPolicy.sandboxMode,
546
682
  executionSelection: input.executionPrompt,
547
683
  role: "coordinator",
548
- evidenceRequired: false,
684
+ evidenceRequired,
549
685
  capabilityEnvelope: {
550
686
  mcpEnabled: capabilityInjection.mcpServers,
551
687
  skillsEnabled: capabilityInjection.skills,
@@ -566,14 +702,19 @@ export function buildNativeRootLoopTurnNode(input) {
566
702
  && compiled.runtimeSidecar.requiredMcp.length > 0,
567
703
  requiresToolCalling: capabilityInjection.requiresToolCalling,
568
704
  });
705
+ const promptHash = hashPrompt(compiled.modelPrompt);
706
+ const promptPayloadRef = input.runId
707
+ ? writePrivatePromptArtifact({ runId: input.runId, nodeId: id, promptHash, userPrompt: input.prompt, compiledPrompt: compiled.modelPrompt })
708
+ : undefined;
569
709
  return {
570
710
  id,
571
- name: compiled.modelPrompt,
711
+ name: redactedPromptLabel(input.prompt),
572
712
  role: "coordinator",
573
713
  dependsOn: [],
574
714
  status: "running",
575
715
  retries: 0,
576
716
  maxRetries: 1,
717
+ outputs: nativeEvidenceOutputForRisk(turnRisk),
577
718
  routing: applyCapabilityInjectionToRouting({
578
719
  provider: input.bootstrap.provider,
579
720
  providerModel: input.bootstrap.selectedModel,
@@ -582,7 +723,11 @@ export function buildNativeRootLoopTurnNode(input) {
582
723
  contextBudget: "normal",
583
724
  readOnly: routingPolicy.readOnly,
584
725
  risk: turnRisk,
585
- promptMode: "dnc-nlp",
726
+ riskTrace,
727
+ evidenceRequired,
728
+ promptMode: promptPayloadRef ? "structured-private" : "dnc-nlp",
729
+ promptHash,
730
+ ...(promptPayloadRef && { promptPayloadRef, promptPayloadPrivate: true }),
586
731
  runtimeSidecar: {
587
732
  ...compiled.runtimeSidecar,
588
733
  requiredMcp: [...compiled.runtimeSidecar.requiredMcp],
@@ -597,6 +742,137 @@ export function buildNativeRootLoopTurnNode(input) {
597
742
  }, selectedCapabilityInjection),
598
743
  };
599
744
  }
745
+ async function writeTurnRoutingArtifact(runId, node, runContext) {
746
+ const artifact = getRunArtifactPath(runId, `turns/${sanitizeRunIdForPath(node.id)}-routing.json`);
747
+ const payload = {
748
+ schemaVersion: "omk.native-turn.routing.v1",
749
+ nodeId: node.id,
750
+ runId,
751
+ workerRunId: runContext?.runId,
752
+ workerOwner: runContext?.worker?.owner,
753
+ routing: node.routing,
754
+ outputs: node.outputs,
755
+ timestamp: new Date().toISOString(),
756
+ };
757
+ try {
758
+ await mkdir(dirname(artifact), { recursive: true });
759
+ await writeFile(artifact, JSON.stringify(payload, null, 2), "utf-8");
760
+ return undefined;
761
+ }
762
+ catch (err) {
763
+ // Non-fatal: artifact write failures must not block turn execution, but must be observable.
764
+ return { error: err instanceof Error ? err.message : String(err) };
765
+ }
766
+ }
767
+ async function appendTurnResultArtifact(runId, node, result) {
768
+ const artifact = getRunArtifactPath(runId, `turns/${sanitizeRunIdForPath(node.id)}-result.jsonl`);
769
+ const payload = {
770
+ schemaVersion: "omk.native-turn.result.v1",
771
+ nodeId: node.id,
772
+ runId,
773
+ exitCode: result.exitCode,
774
+ success: result.success,
775
+ evidenceRequired: result.metadata?.evidenceRequired,
776
+ timestamp: new Date().toISOString(),
777
+ };
778
+ try {
779
+ await mkdir(dirname(artifact), { recursive: true });
780
+ await appendFile(artifact, `${JSON.stringify(payload)}\n`, "utf-8");
781
+ return { artifact };
782
+ }
783
+ catch (err) {
784
+ // Non-fatal: artifact write failures must not block turn execution, but must be observable.
785
+ return { error: err instanceof Error ? err.message : String(err) };
786
+ }
787
+ }
788
+ function sanitizeRunIdForPath(value) {
789
+ return value.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 64);
790
+ }
791
+ async function sha256File(path) {
792
+ try {
793
+ return createHash("sha256").update(await readFile(path)).digest("hex");
794
+ }
795
+ catch {
796
+ return undefined;
797
+ }
798
+ }
799
+ function parseReplayIndex(raw, runId) {
800
+ try {
801
+ const parsed = JSON.parse(raw);
802
+ if (parsed.schemaVersion !== "omk.replay-index.v1" || parsed.runId !== runId || !Array.isArray(parsed.artifacts)) {
803
+ throw new Error("invalid replay index");
804
+ }
805
+ return {
806
+ schemaVersion: "omk.replay-index.v1",
807
+ runId,
808
+ updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : new Date().toISOString(),
809
+ artifacts: parsed.artifacts.filter((artifact) => Boolean(artifact && typeof artifact.path === "string" && typeof artifact.kind === "string")),
810
+ };
811
+ }
812
+ catch {
813
+ return { schemaVersion: "omk.replay-index.v1", runId, updatedAt: new Date().toISOString(), artifacts: [] };
814
+ }
815
+ }
816
+ async function upsertReplayIndexArtifact(input) {
817
+ const now = new Date().toISOString();
818
+ let existing = { schemaVersion: "omk.replay-index.v1", runId: input.runId, updatedAt: now, artifacts: [] };
819
+ try {
820
+ existing = parseReplayIndex(await readFile(input.replayIndexPath, "utf-8"), input.runId);
821
+ }
822
+ catch {
823
+ // Missing replay index is expected for the first turn.
824
+ }
825
+ const artifacts = existing.artifacts.filter((artifact) => artifact.path !== input.artifactPath);
826
+ artifacts.push({ path: input.artifactPath, sha256: input.artifactHash, kind: input.artifactKind, updatedAt: now });
827
+ await writeFile(input.replayIndexPath, JSON.stringify({
828
+ schemaVersion: "omk.replay-index.v1",
829
+ runId: input.runId,
830
+ updatedAt: now,
831
+ artifacts,
832
+ }, null, 2), "utf-8");
833
+ }
834
+ async function recordTurnAuditGraph(input) {
835
+ try {
836
+ const artifactHash = input.resultArtifact ? await sha256File(input.resultArtifact) : undefined;
837
+ const replayIndex = getRunArtifactPath(input.runId, "replay-index.json", input.root);
838
+ await mkdir(dirname(replayIndex), { recursive: true });
839
+ if (input.resultArtifact) {
840
+ await upsertReplayIndexArtifact({
841
+ replayIndexPath: replayIndex,
842
+ runId: input.runId,
843
+ artifactPath: input.resultArtifact,
844
+ artifactHash,
845
+ artifactKind: "turn-result",
846
+ });
847
+ }
848
+ const store = await LocalGraphMemoryStore.create({ projectRoot: input.root, sessionId: input.runId, source: "native-root-loop-audit" });
849
+ await store?.materializeTurnAudit({
850
+ runId: input.runId,
851
+ nodeId: input.node.id,
852
+ provider: String(input.node.routing?.provider ?? "auto"),
853
+ selectedRuntime: typeof input.result.metadata?.selectedRuntime === "string"
854
+ ? input.result.metadata.selectedRuntime
855
+ : typeof input.result.metadata?.runtime === "string"
856
+ ? input.result.metadata.runtime
857
+ : undefined,
858
+ fallbackChain: Array.isArray(input.result.metadata?.fallbackChain)
859
+ ? input.result.metadata.fallbackChain.filter((entry) => typeof entry === "string")
860
+ : undefined,
861
+ evidenceKind: input.result.success ? "turn-result-pass" : "turn-result-fail",
862
+ evidenceArtifactPath: input.resultArtifact,
863
+ evidenceHash: artifactHash,
864
+ evidenceRequirements: evidenceRequirementsFromOutputs(input.node.outputs),
865
+ evidenceObservations: evidenceObservationsFromResult({
866
+ metadata: input.result.metadata,
867
+ stdout: input.result.stdout,
868
+ artifactPaths: input.resultArtifact ? [input.resultArtifact] : [],
869
+ }),
870
+ });
871
+ }
872
+ catch {
873
+ // Best-effort audit graph materialization must not block turn completion.
874
+ }
875
+ }
600
876
  function runtimeSidecarIntent(node) {
601
877
  const sidecar = node.routing?.runtimeSidecar;
602
878
  if (!sidecar || typeof sidecar !== "object")
@@ -617,12 +893,34 @@ function describeNativeTurnActivity(node) {
617
893
  : "local ctx";
618
894
  return `${intentPart} · ${guardPart} · ${scopePart} · ${provider}`;
619
895
  }
620
- async function executeNativeRootTurn(input) {
896
+ export async function executeNativeRootTurn(input) {
621
897
  const startedAt = Date.now();
622
898
  const routing = input.node.routing;
623
899
  const activity = describeNativeTurnActivity(input.node);
624
900
  emitNativeTurnRoute(input);
625
- recordNativeTurnToolAuthority({ node: input.node, env: input.env, renderer: input.renderer });
901
+ const authorityRecord = recordNativeTurnToolAuthority({ node: input.node, env: input.env, renderer: input.renderer });
902
+ if (authorityRecord.enforced) {
903
+ const message = `${authorityRecord.reason}; native turn dispatch blocked`;
904
+ if (input.renderer) {
905
+ input.renderer.emit({ type: "turn:error", message });
906
+ }
907
+ else {
908
+ process.stderr.write(style.metricsRed(message) + "\n");
909
+ }
910
+ return {
911
+ success: false,
912
+ exitCode: 78,
913
+ stdout: "",
914
+ stderr: message,
915
+ metadata: {
916
+ code: "TOOL_AUTHORITY_BLOCKED",
917
+ nodeId: input.node.id,
918
+ op: authorityRecord.op,
919
+ decision: authorityRecord.decision,
920
+ authorityMode: authorityRecord.mode,
921
+ },
922
+ };
923
+ }
626
924
  let heartbeatPrinted = false;
627
925
  let heartbeatLineClosed = false;
628
926
  const heartbeat = input.heartbeatEnabled
@@ -644,8 +942,29 @@ async function executeNativeRootTurn(input) {
644
942
  }, 3000)
645
943
  : undefined;
646
944
  heartbeat?.unref?.();
945
+ const diagnostics = {};
647
946
  try {
947
+ const turnRunId = input.env.OMK_RUN_ID ?? input.runContext?.goal.runId ?? input.node.id;
948
+ const routingWrite = await writeTurnRoutingArtifact(turnRunId, input.node, input.runContext);
949
+ if (routingWrite?.error)
950
+ diagnostics.routingWrite = routingWrite.error;
648
951
  const result = await input.taskRunner.run(input.node, input.env, input.signal, input.runContext);
952
+ const resultWrite = await appendTurnResultArtifact(turnRunId, input.node, result);
953
+ if (resultWrite?.error)
954
+ diagnostics.resultWrite = resultWrite.error;
955
+ await recordTurnAuditGraph({
956
+ root: input.env.OMK_PROJECT_ROOT ?? process.cwd(),
957
+ runId: turnRunId,
958
+ node: input.node,
959
+ result,
960
+ resultArtifact: resultWrite?.artifact,
961
+ });
962
+ if (Object.keys(diagnostics).length > 0) {
963
+ result.metadata = {
964
+ ...(result.metadata ?? {}),
965
+ artifactWriteDiagnostics: diagnostics,
966
+ };
967
+ }
649
968
  if (input.heartbeatEnabled) {
650
969
  const sec = ((Date.now() - startedAt) / 1000).toFixed(1);
651
970
  if (input.renderer) {
@@ -690,6 +1009,7 @@ async function buildNativeRootLoopTurnDag(input) {
690
1009
  skillNames: input.skillNames,
691
1010
  hookNames: input.hookNames,
692
1011
  executionPrompt: input.executionPrompt,
1012
+ runId: input.runId,
693
1013
  });
694
1014
  return createDag({ nodes: [node] });
695
1015
  }
@@ -724,6 +1044,7 @@ async function executeNativeRootHarnessTurn(input) {
724
1044
  hookNames: input.hookNames,
725
1045
  executionPrompt: input.executionPrompt,
726
1046
  workers: input.workers,
1047
+ runId: input.runId,
727
1048
  });
728
1049
  const completed = [];
729
1050
  const workerCount = Math.max(1, input.workers ?? 1);
@@ -5,6 +5,7 @@ import { getProjectRoot, pathExists, readTextFile, getOmkPath, getRunPath, getRu
5
5
  import { createDag } from "../orchestration/dag.js";
6
6
  import { createStatePersister } from "../orchestration/state-persister.js";
7
7
  import { checkEvidenceGates } from "../orchestration/evidence-gate.js";
8
+ import { checkEvidenceGate } from "../runtime/contracts/evidence.js";
8
9
  import { createExecutor } from "../orchestration/executor.js";
9
10
  import { createExecutableDagFromState, routeRunState } from "../orchestration/run-state.js";
10
11
  import { createOmkSessionEnv } from "../util/session.js";
@@ -126,6 +127,16 @@ export async function dagReplayCommand(runId, target, subtarget, options) {
126
127
  case "test-pass":
127
128
  gates.push({ type: "command-pass", command: output.ref ?? "npm test" });
128
129
  break;
130
+ case "command-pass":
131
+ gates.push({ type: "command-pass", command: output.ref ?? "" });
132
+ break;
133
+ case "artifact":
134
+ if (output.ref)
135
+ gates.push({ type: "file-exists", path: output.ref });
136
+ break;
137
+ case "diff":
138
+ gates.push({ type: "diff-nonempty" });
139
+ break;
129
140
  case "review-pass":
130
141
  case "summary":
131
142
  gates.push({ type: "summary-present", summaryMarker: output.ref ?? "## Summary" });
@@ -134,6 +145,11 @@ export async function dagReplayCommand(runId, target, subtarget, options) {
134
145
  break;
135
146
  }
136
147
  }
148
+ if (node.routing?.evidenceRequired === true && gates.length === 0) {
149
+ console.log(style.pink(` ✗ ${node.id}: evidence-required — no replayable evidence gates declared`));
150
+ failedCount++;
151
+ continue;
152
+ }
137
153
  if (gates.length === 0)
138
154
  continue;
139
155
  const result = await checkEvidenceGates(gates, {
@@ -141,6 +157,35 @@ export async function dagReplayCommand(runId, target, subtarget, options) {
141
157
  stdout: "",
142
158
  nodeId: node.id,
143
159
  });
160
+ const evidenceGates = [];
161
+ const artifactPaths = [];
162
+ const metadata = {};
163
+ for (const ev of result.evidence) {
164
+ if (!ev.passed)
165
+ continue;
166
+ if (ev.gate === "command-pass")
167
+ evidenceGates.push("command-pass");
168
+ else if (ev.gate === "summary-present")
169
+ evidenceGates.push("summary");
170
+ else if (ev.gate === "file-exists") {
171
+ evidenceGates.push("artifact");
172
+ if (ev.ref)
173
+ artifactPaths.push(ev.ref);
174
+ }
175
+ else if (ev.gate === "diff-nonempty") {
176
+ evidenceGates.push("diff");
177
+ metadata.diff = true;
178
+ }
179
+ }
180
+ if (evidenceGates.length > 0)
181
+ metadata.evidenceGates = evidenceGates;
182
+ const v2 = checkEvidenceGate(true, node.outputs, metadata, "", artifactPaths);
183
+ if (!v2.satisfied) {
184
+ for (const gate of v2.missing) {
185
+ console.log(style.pink(` ✗ ${node.id}: ${gate} — ${v2.reason}`));
186
+ failedCount++;
187
+ }
188
+ }
144
189
  for (const ev of result.evidence) {
145
190
  const icon = ev.passed ? "✓" : "✗";
146
191
  const color = ev.passed ? style.mint : style.pink;