dsh-omni-router 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,6 +79,7 @@
79
79
  - **2.1 收敛增强**:Mission Resume(`omni_mission_resume` 跨 session 继续 DAG,`runDagLoop` 支持 `onProgress` 周期保存)、Harness evidence extraction(`extractHarnessEvidence` 从 commands/tests/toolCalls/EVIDENCE_JSON 提取机器可验证证据)、context token budget(`maxContextTokens` 真正限制检索输出)、semantic failure retrieval + evidence-backed skill distillation、OmniBench v2 real runner。
80
80
  - **2.2 Capability Auto-Provisioning(优化7)**:Capability Auditor(baseline/task-time audit)、Gap Detection、Plugin/Skill Quality Scoring、Minimal Capability Set Solver、Discovery Adapters(marketplace/hub/static registry)、Trusted Auto Provisioning(recommend/auto-trusted/manual)、Post-install probe、Install rollback transaction、Capability performance learning / redundancy detection。
81
81
  - **2.3 Progressive Intelligence(优化8)**:L0 Direct / L1 Assisted / L2 Orchestrated / L3 Guarded 四级渐进智能;Context Capsule(最小相关上下文 + 动态 needs);Strategy Shift(相同失败策略自动换策略,不再盲目 retry);`omni_explain` / `omni_doctor` 用户入口。
82
+ - **2.4 Reliability Kernel(优化9)**:TaskContract 统一任务契约;Intervention Gate(默认 NOOP,按 Expected Utility 决定是否介入);OmniEvent / Host Interface(宿主无关事件与能力协商);Evidence Trust(T0-T4)与 Invalidation;Context Freshness(workspace fingerprint);Recovery Policy(retry/repair/expand/replan/escalate/stop)。
82
83
  - **TaskDecision**:`createTaskDecision` 生成唯一决策对象,Policy/Runtime 统一消费,避免重复 classify 分叉。
83
84
  - **Evidence Protocol**:`src/evidence.mjs` 用结构化 command/file/test/finding 证据判定 PASS/FAIL;`isQaPass` 已支持从 QA 输出解析 JSON evidence,不再只信文本。
84
85
  - **Task Compiler**:`compileTask` 生成 objective / constraints / non-goals / acceptance / hidden assumptions / ambiguities / invariants / risk / artifacts;`compileTaskWithLLM` 可用 LLM 增强。
package/README.zh-CN.md CHANGED
@@ -79,6 +79,7 @@
79
79
  - **2.1 收敛增强**:Mission Resume(`omni_mission_resume` 跨 session 继续 DAG,`runDagLoop` 支持 `onProgress` 周期保存)、Harness evidence extraction(`extractHarnessEvidence` 从 commands/tests/toolCalls/EVIDENCE_JSON 提取机器可验证证据)、context token budget(`maxContextTokens` 真正限制检索输出)、semantic failure retrieval + evidence-backed skill distillation、OmniBench v2 real runner。
80
80
  - **2.2 Capability Auto-Provisioning(优化7)**:Capability Auditor(baseline/task-time audit)、Gap Detection、Plugin/Skill Quality Scoring、Minimal Capability Set Solver、Discovery Adapters(marketplace/hub/static registry)、Trusted Auto Provisioning(recommend/auto-trusted/manual)、Post-install probe、Install rollback transaction、Capability performance learning / redundancy detection。
81
81
  - **2.3 Progressive Intelligence(优化8)**:L0 Direct / L1 Assisted / L2 Orchestrated / L3 Guarded 四级渐进智能;Context Capsule(最小相关上下文 + 动态 needs);Strategy Shift(相同失败策略自动换策略,不再盲目 retry);`omni_explain` / `omni_doctor` 用户入口。
82
+ - **2.4 Reliability Kernel(优化9)**:TaskContract 统一任务契约;Intervention Gate(默认 NOOP,按 Expected Utility 决定是否介入);OmniEvent / Host Interface(宿主无关事件与能力协商);Evidence Trust(T0-T4)与 Invalidation;Context Freshness(workspace fingerprint);Recovery Policy(retry/repair/expand/replan/escalate/stop)。
82
83
  - **TaskDecision**:`createTaskDecision` 生成唯一决策对象,Policy/Runtime 统一消费,避免重复 classify 分叉。
83
84
  - **Evidence Protocol**:`src/evidence.mjs` 用结构化 command/file/test/finding 证据判定 PASS/FAIL;`isQaPass` 已支持从 QA 输出解析 JSON evidence,不再只信文本。
84
85
  - **Task Compiler**:`compileTask` 生成 objective / constraints / non-goals / acceptance / hidden assumptions / ambiguities / invariants / risk / artifacts;`compileTaskWithLLM` 可用 LLM 增强。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-omni-router",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "Omni - a reliability and orchestration control plane for DeepSeek Harness, designed to turn fast models into dependable long-horizon software-engineering agents.",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -41,7 +41,7 @@
41
41
  }
42
42
  },
43
43
  "scripts": {
44
- "test": "node test/omni-router.test.mjs && node test/agent-chain.test.mjs && node test/skill-suggest.test.mjs && node test/methodology.test.mjs && node test/compat.test.mjs && node test/project-brain.test.mjs && node test/mission-planner.test.mjs && node test/agent-runtime.test.mjs && node test/judge.test.mjs && node test/memory.test.mjs && node test/memory-v3.test.mjs && node test/memory-engine.test.mjs && node test/mission-resume.test.mjs && node test/engineering-benchmark.test.mjs && node test/benchmark-runner.test.mjs && node test/benchmark-results.test.mjs && node test/continuous-eval.test.mjs && node --no-warnings test/project-brain-v2.test.mjs && node --no-warnings test/project-brain-v3.test.mjs && node test/visual-qa.test.mjs && node test/evidence.test.mjs && node test/evidence-store.test.mjs && node test/evidence-engine.test.mjs && node test/task-decision.test.mjs && node test/task-compiler.test.mjs && node test/capability-brain.test.mjs && node test/capability-manifest.test.mjs && node test/capability-sandbox.test.mjs && node test/capability-auditor.test.mjs && node test/capability-quality.test.mjs && node test/capability-solver.test.mjs && node test/capability-provisioner.test.mjs && node test/capability-performance.test.mjs && node test/progressive-intelligence.test.mjs && node test/context-capsule.test.mjs && node test/strategy-shift.test.mjs && node test/planner-dag.test.mjs && node test/dynamic-context.test.mjs && node test/mission-dag.test.mjs && node test/failure-taxonomy.test.mjs && node test/context-expansion.test.mjs && node test/hybrid-retrieval.test.mjs && node test/dependency-graph.test.mjs && node test/ast-provider.test.mjs && node test/experience-router.test.mjs && node test/benchmark-tasks.test.mjs && node test/bundle-installer.test.mjs && node test/omnibench-runner.test.mjs",
44
+ "test": "node test/omni-router.test.mjs && node test/agent-chain.test.mjs && node test/skill-suggest.test.mjs && node test/methodology.test.mjs && node test/compat.test.mjs && node test/project-brain.test.mjs && node test/mission-planner.test.mjs && node test/agent-runtime.test.mjs && node test/judge.test.mjs && node test/memory.test.mjs && node test/memory-v3.test.mjs && node test/memory-engine.test.mjs && node test/mission-resume.test.mjs && node test/engineering-benchmark.test.mjs && node test/benchmark-runner.test.mjs && node test/benchmark-results.test.mjs && node test/continuous-eval.test.mjs && node --no-warnings test/project-brain-v2.test.mjs && node --no-warnings test/project-brain-v3.test.mjs && node test/visual-qa.test.mjs && node test/evidence.test.mjs && node test/evidence-store.test.mjs && node test/evidence-engine.test.mjs && node test/task-decision.test.mjs && node test/task-compiler.test.mjs && node test/capability-brain.test.mjs && node test/capability-manifest.test.mjs && node test/capability-sandbox.test.mjs && node test/capability-auditor.test.mjs && node test/capability-quality.test.mjs && node test/capability-solver.test.mjs && node test/capability-provisioner.test.mjs && node test/capability-performance.test.mjs && node test/progressive-intelligence.test.mjs && node test/context-capsule.test.mjs && node test/strategy-shift.test.mjs && node test/task-contract.test.mjs && node test/intervention-gate.test.mjs && node test/omni-event.test.mjs && node test/host-interface.test.mjs && node test/evidence-trust.test.mjs && node test/context-freshness.test.mjs && node test/recovery-policy.test.mjs && node test/planner-dag.test.mjs && node test/dynamic-context.test.mjs && node test/mission-dag.test.mjs && node test/failure-taxonomy.test.mjs && node test/context-expansion.test.mjs && node test/hybrid-retrieval.test.mjs && node test/dependency-graph.test.mjs && node test/ast-provider.test.mjs && node test/experience-router.test.mjs && node test/benchmark-tasks.test.mjs && node test/bundle-installer.test.mjs && node test/omnibench-runner.test.mjs",
45
45
  "benchmark": "node benchmark/run.mjs",
46
46
  "benchmark:engineering": "node benchmark/engineering-run.mjs",
47
47
  "benchmark:compare": "node benchmark/compare.mjs",
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Context Freshness.
3
+ *
4
+ * Context Capsules and evidence records must be bound to a workspace
5
+ * fingerprint. After files change, old context/evidence is stale and cannot be
6
+ * used as reliable input.
7
+ */
8
+
9
+ import crypto from 'node:crypto'
10
+
11
+ export function fingerprintText(text) {
12
+ return crypto.createHash('sha1').update(String(text || '')).digest('hex').slice(0, 16)
13
+ }
14
+
15
+ export function fingerprintFiles(files = {}) {
16
+ const hash = crypto.createHash('sha1')
17
+ for (const [name, content] of Object.entries(files)) {
18
+ hash.update(name)
19
+ hash.update('\0')
20
+ hash.update(String(content || ''))
21
+ hash.update('\0')
22
+ }
23
+ return hash.digest('hex').slice(0, 24)
24
+ }
25
+
26
+ export function fingerprintWorkspace({ commit = '', files = {} } = {}) {
27
+ return fingerprintText(`${commit || ''}|${fingerprintFiles(files)}`)
28
+ }
29
+
30
+ export function attachFingerprint(context = {}, fingerprint = '') {
31
+ return { ...context, workspaceFingerprint: fingerprint }
32
+ }
33
+
34
+ export function isContextStale(context = {}, currentFingerprint = '') {
35
+ if (!currentFingerprint) return false
36
+ return Boolean(context.workspaceFingerprint && context.workspaceFingerprint !== currentFingerprint)
37
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Evidence Trust & Invalidation.
3
+ *
4
+ * Assigns every evidence record a trust level and a workspace fingerprint.
5
+ * If the workspace changes after evidence was produced, that evidence becomes
6
+ * stale and must not count toward completion.
7
+ */
8
+
9
+ export const TRUST_LEVELS = {
10
+ T0: 0, // model claim
11
+ T1: 1, // agent observation
12
+ T2: 2, // host/tool output
13
+ T3: 3, // deterministic execution
14
+ T4: 4, // independent/hidden verifier
15
+ }
16
+
17
+ export function createEvidenceRecord({
18
+ id = `E-${Date.now().toString(36)}`,
19
+ criterionId = '',
20
+ workspaceFingerprint = '',
21
+ producer = 'unknown',
22
+ trustLevel = 'T0',
23
+ createdAt = new Date().toISOString(),
24
+ payload = {},
25
+ } = {}) {
26
+ return {
27
+ evidenceId: id,
28
+ criterionId,
29
+ workspaceFingerprint,
30
+ producer,
31
+ trustLevel,
32
+ trustValue: TRUST_LEVELS[trustLevel] ?? 0,
33
+ createdAt,
34
+ payload,
35
+ }
36
+ }
37
+
38
+ export function isEvidenceStale(record = {}, currentFingerprint = '') {
39
+ if (!currentFingerprint) return false
40
+ return Boolean(record.workspaceFingerprint && record.workspaceFingerprint !== currentFingerprint)
41
+ }
42
+
43
+ export function invalidateEvidence(records = [], currentFingerprint = '') {
44
+ return records.map((record) => ({
45
+ ...record,
46
+ stale: isEvidenceStale(record, currentFingerprint),
47
+ }))
48
+ }
49
+
50
+ export function requiredTrustForRisk(risk = 'low') {
51
+ if (risk === 'high' || risk === 'critical') return { minimum: 'T3', independent: true, label: 'T3 + independent T4' }
52
+ if (risk === 'medium') return { minimum: 'T3', independent: false, label: 'T3' }
53
+ return { minimum: 'T1', independent: false, label: 'T1/T2' }
54
+ }
55
+
56
+ export function evidenceMeetsTrust(record = {}, requiredLevel = 'T0') {
57
+ const required = TRUST_LEVELS[requiredLevel] ?? 0
58
+ return (record.trustValue ?? 0) >= required
59
+ }
60
+
61
+ export function formatEvidenceTrust(record = {}) {
62
+ return `E${record.evidenceId || '?'} T${record.trustLevel || '0'} ${record.stale ? 'STALE' : 'fresh'} criterion=${record.criterionId || '-'}`
63
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * OmniHost Interface.
3
+ *
4
+ * The small contract a host (DSH, Codex, Claude, ...) implements so Omni's
5
+ * kernel stays host-independent. Omni never executes; it decides, prepares,
6
+ * and verifies through this boundary.
7
+ */
8
+
9
+ export const HOST_FEATURES = [
10
+ 'workflow',
11
+ 'approvals',
12
+ 'skills',
13
+ 'plugins',
14
+ 'subagents',
15
+ 'toolEvents',
16
+ 'testEvents',
17
+ 'fileEvents',
18
+ ]
19
+
20
+ export function createHostAdapter(host = {}) {
21
+ const missing = []
22
+ for (const method of ['describeHost']) {
23
+ if (typeof host[method] !== 'function') missing.push(method)
24
+ }
25
+ return {
26
+ ...host,
27
+ missing,
28
+ valid: missing.length === 0,
29
+ }
30
+ }
31
+
32
+ export function describeHostCapabilities(capabilities = {}) {
33
+ const result = {}
34
+ for (const feature of HOST_FEATURES) {
35
+ result[feature] = capabilities[feature] === true
36
+ }
37
+ return result
38
+ }
39
+
40
+ export function negotiateHost(hostCapabilities = {}, required = HOST_FEATURES) {
41
+ const caps = describeHostCapabilities(hostCapabilities)
42
+ const supported = []
43
+ const degraded = []
44
+ for (const feature of required) {
45
+ if (caps[feature]) supported.push(feature)
46
+ else degraded.push(feature)
47
+ }
48
+ return {
49
+ supported,
50
+ degraded,
51
+ capabilities: caps,
52
+ mode: degraded.length ? 'degraded' : 'full',
53
+ }
54
+ }
55
+
56
+ export function formatHostNegotiation(result = {}) {
57
+ return [
58
+ `Host mode: ${result.mode}`,
59
+ `Supported: ${result.supported.join(', ') || '(none)'}`,
60
+ `Degraded: ${result.degraded.join(', ') || '(none)'}`,
61
+ ].join('\n')
62
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Intervention Gate.
3
+ *
4
+ * Decides whether Omni should intervene at all. The default answer is NOOP:
5
+ * if the host can already do the job without extra tokens/latency/context
6
+ * pollution, Omni stays out of the way.
7
+ */
8
+
9
+ export function expectedInterventionUtility({
10
+ successGain = 0,
11
+ tokenOverhead = 0,
12
+ latency = 0,
13
+ contextPollution = 0,
14
+ overlap = 0,
15
+ failureSurface = 0,
16
+ taskFrequency = 1,
17
+ } = {}) {
18
+ const utility = (
19
+ successGain * 1.0 -
20
+ tokenOverhead * 0.5 -
21
+ latency * 0.3 -
22
+ contextPollution * 0.5 -
23
+ overlap * 0.4 -
24
+ failureSurface * 0.4
25
+ ) * Math.max(0.1, taskFrequency)
26
+ return Math.round(utility * 1000) / 1000
27
+ }
28
+
29
+ export function shouldIntervene(utility = 0, threshold = 0.1) {
30
+ return utility > threshold
31
+ }
32
+
33
+ export function decideIntervention({
34
+ successGain,
35
+ tokenOverhead,
36
+ latency,
37
+ contextPollution,
38
+ overlap,
39
+ failureSurface,
40
+ taskFrequency,
41
+ threshold = 0.1,
42
+ } = {}) {
43
+ const utility = expectedInterventionUtility({
44
+ successGain,
45
+ tokenOverhead,
46
+ latency,
47
+ contextPollution,
48
+ overlap,
49
+ failureSurface,
50
+ taskFrequency,
51
+ })
52
+ return {
53
+ utility,
54
+ threshold,
55
+ intervene: shouldIntervene(utility, threshold),
56
+ mode: shouldIntervene(utility, threshold) ? 'intervene' : 'noop',
57
+ }
58
+ }
59
+
60
+ export function noOpPrecision(decisions = []) {
61
+ // decision: { predictedNoop: boolean, actuallyNeeded: boolean }
62
+ let predictedNoop = 0
63
+ let correctNoop = 0
64
+ for (const d of decisions) {
65
+ if (!d.predictedNoop) continue
66
+ predictedNoop += 1
67
+ if (!d.actuallyNeeded) correctNoop += 1
68
+ }
69
+ return predictedNoop ? Math.round((correctNoop / predictedNoop) * 1000) / 1000 : 1
70
+ }
71
+
72
+ export function interventionEfficiency({ rawSuccess = 0, omniSuccess = 0, tokenOverhead = 0 }) {
73
+ const gain = omniSuccess - rawSuccess
74
+ const overhead = Math.max(0.01, tokenOverhead)
75
+ return {
76
+ gain: Math.round(gain * 1000) / 1000,
77
+ tokenOverhead,
78
+ efficiency: Math.round((gain / overhead) * 1000) / 1000,
79
+ }
80
+ }
81
+
82
+ export function formatInterventionGate(result = {}) {
83
+ return `${result.intervene ? 'INTERVENE' : 'NOOP'} (utility=${result.utility}, threshold=${result.threshold})`
84
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * OmniEvent.
3
+ *
4
+ * Host-independent event model. DSH, Codex, Claude, etc. normalize their own
5
+ * events into OmniEvent so the Evidence/Recovery/Memory kernels never depend on
6
+ * a specific host.
7
+ */
8
+
9
+ export const OMNI_EVENT_TYPES = [
10
+ 'model.started',
11
+ 'model.completed',
12
+ 'tool.started',
13
+ 'tool.completed',
14
+ 'command.completed',
15
+ 'test.completed',
16
+ 'file.changed',
17
+ 'approval.requested',
18
+ 'approval.completed',
19
+ 'agent.started',
20
+ 'agent.completed',
21
+ ]
22
+
23
+ export function createOmniEvent({
24
+ type,
25
+ host = 'unknown',
26
+ sessionId = '',
27
+ taskId = '',
28
+ workspaceFingerprint = '',
29
+ payload = {},
30
+ } = {}) {
31
+ if (!type) throw new Error('OmniEvent type is required')
32
+ return {
33
+ type,
34
+ host,
35
+ sessionId,
36
+ taskId,
37
+ workspaceFingerprint,
38
+ timestamp: new Date().toISOString(),
39
+ payload,
40
+ }
41
+ }
42
+
43
+ export function normalizeHostEvent(hostEvent = {}, host = 'unknown', context = {}) {
44
+ const rawType = String(hostEvent.type || hostEvent.event || hostEvent.name || 'unknown')
45
+ const type = mapHostEventType(rawType, host)
46
+ return createOmniEvent({
47
+ type,
48
+ host: hostEvent.host || host,
49
+ sessionId: hostEvent.sessionId || context.sessionId || '',
50
+ taskId: hostEvent.taskId || context.taskId || '',
51
+ workspaceFingerprint: hostEvent.workspaceFingerprint || context.workspaceFingerprint || '',
52
+ payload: hostEvent.payload || hostEvent.data || hostEvent,
53
+ })
54
+ }
55
+
56
+ function mapHostEventType(raw, host) {
57
+ const value = String(raw).toLowerCase()
58
+ if (host === 'dsh') {
59
+ if (/tool|command/.test(value) && /start/.test(value)) return 'tool.started'
60
+ if (/tool|command/.test(value) && /end|done|complete/.test(value)) return 'tool.completed'
61
+ if (/test/.test(value)) return 'test.completed'
62
+ if (/file|change/.test(value)) return 'file.changed'
63
+ if (/approval/.test(value)) return 'approval.requested'
64
+ if (/agent|subagent/.test(value) && /start/.test(value)) return 'agent.started'
65
+ if (/agent|subagent/.test(value) && /end|done|complete/.test(value)) return 'agent.completed'
66
+ }
67
+ if (/start|begin/.test(value)) return 'model.started'
68
+ if (/end|done|complete|finish/.test(value)) return 'model.completed'
69
+ return OMNI_EVENT_TYPES.includes(raw) ? raw : 'model.completed'
70
+ }
@@ -39,6 +39,10 @@ import { baselineAudit, formatCapabilityAudit, taskTimeAudit } from './capabilit
39
39
  import { createStaticRegistryAdapter, discoverCandidates, evaluateProvisionPlan, formatProvisionResult, probeCapability, provisionCapabilities } from './capability-provisioner.mjs'
40
40
  import { evaluateProviderValue, formatPerformanceRegistry, loadPerformanceRegistry, recommendDemotion, recordProvisionOutcome, savePerformanceRegistry } from './capability-performance.mjs'
41
41
  import { decideIntelligenceLevel, formatIntelligenceLevel } from './progressive-intelligence.mjs'
42
+ import { buildTaskContract, formatTaskContract } from './task-contract.mjs'
43
+ import { decideIntervention, formatInterventionGate } from './intervention-gate.mjs'
44
+ import { negotiateHost, formatHostNegotiation } from './host-interface.mjs'
45
+ import { requiredTrustForRisk } from './evidence-trust.mjs'
42
46
  import { buildProgressiveContext } from './context-expansion.mjs'
43
47
  import { buildDynamicContext } from './dynamic-context.mjs'
44
48
  import { classifyFailure } from './failure-taxonomy.mjs'
@@ -1020,12 +1024,18 @@ export function apply(ctx, config = {}) {
1020
1024
  thinkingMode: state.thinkingMode || 'balanced',
1021
1025
  })
1022
1026
  const intelligence = decideIntelligenceLevel(taskDecision)
1027
+ const contract = buildTaskContract({ taskText: state.firstText || '', decision: taskDecision })
1028
+ const intervention = decideIntervention({
1029
+ successGain: intelligence.level === 'L0' ? 0 : 0.15,
1030
+ tokenOverhead: intelligence.level === 'L0' ? 0.05 : 0.2,
1031
+ })
1023
1032
  return [
1024
1033
  `omni-router: ${state.kind || 'unclassified'}`,
1025
1034
  `taskType=${state.taskType || 'unknown'}`,
1026
1035
  `thinkingMode=${state.thinkingMode || 'balanced'}`,
1027
1036
  `riskLevel=${state.riskLevel || 'unknown'}`,
1028
1037
  `intelligenceLevel=${formatIntelligenceLevel(intelligence)}`,
1038
+ `intervention=${formatInterventionGate(intervention)}`,
1029
1039
  `planRequested=${state.planRequested}`,
1030
1040
  `directOverride=${state.directOverride}`,
1031
1041
  `routerStandard=${routerStandard ? 'delegated' : 'not-detected'}`,
@@ -1054,6 +1064,8 @@ export function apply(ctx, config = {}) {
1054
1064
  thinkingMode: state.thinkingMode || 'balanced',
1055
1065
  })
1056
1066
  const intelligence = decideIntelligenceLevel(decision)
1067
+ const contract = buildTaskContract({ taskText: state.firstText || '', decision })
1068
+ const trust = requiredTrustForRisk(decision.risk)
1057
1069
  const topic = String(args?.topic || '').toLowerCase()
1058
1070
  const lines = [
1059
1071
  `Mode: ${intelligence.level} ${intelligence.label}`,
@@ -1061,8 +1073,9 @@ export function apply(ctx, config = {}) {
1061
1073
  `Reasoning effort: ${intelligence.reasoningEffort}`,
1062
1074
  `Verification: ${intelligence.verification}`,
1063
1075
  `Approval required: ${intelligence.approvalRequired ? 'yes' : 'no'}`,
1064
- `Capabilities needed: ${(decision.evidenceRequirements || []).join(', ') || 'native tools'}`,
1065
- `Completion requires: ${decision.evidenceRequirements?.length ? 'harness evidence for ' + decision.evidenceRequirements.join(', ') : 'light verification'}`,
1076
+ `Capabilities needed: ${(contract.requiredCapabilities || []).join(', ') || 'native tools'}`,
1077
+ `Evidence trust required: ${trust.label}`,
1078
+ `Completion requires: ${contract.acceptance?.join('; ') || 'light verification'}`,
1066
1079
  ]
1067
1080
  if (topic === 'mode' || topic === 'capabilities' || topic === 'verification' || topic === 'completion') {
1068
1081
  const idx = lines.findIndex((l) => l.toLowerCase().startsWith(topic))
@@ -1084,6 +1097,17 @@ export function apply(ctx, config = {}) {
1084
1097
  brain = loadCapabilityManifests(brain, config.capabilityManifests || [])
1085
1098
  const baseline = baselineAudit(brain)
1086
1099
  const fs = ctx.get('fs') || ctx.fs
1100
+ const hostCaps = {
1101
+ workflow: !!(ctx.get('workflow') || ctx.workflow),
1102
+ approvals: !!(ctx.get('approvals') || ctx.approvals),
1103
+ skills: !!(ctx.get('skills') || ctx.skills),
1104
+ plugins: !!(ctx.get('plugins') || ctx.plugins),
1105
+ subagents: !!(ctx.get('subagents') || ctx.subagents),
1106
+ toolEvents: !!(ctx.get('events') || ctx.events),
1107
+ testEvents: false,
1108
+ fileEvents: !!fs,
1109
+ }
1110
+ const host = negotiateHost(hostCaps)
1087
1111
  const lines = [
1088
1112
  `DSH session: ${session ? 'ok' : 'missing'}`,
1089
1113
  `Tools registered: ${toolNames.length}`,
@@ -1091,6 +1115,7 @@ export function apply(ctx, config = {}) {
1091
1115
  `Missing baseline: ${baseline.missing.join(', ') || '(none)'}`,
1092
1116
  `ProjectIndex: ${fs ? 'available' : 'unavailable'}`,
1093
1117
  `Evidence hooks: ${ctx.get('evidence') || ctx.evidence ? 'available' : 'not-detected'}`,
1118
+ `Host negotiation: ${host.mode} (degraded: ${host.degraded.join(', ') || 'none'})`,
1094
1119
  ]
1095
1120
  return lines.join('\n')
1096
1121
  },
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Recovery Policy.
3
+ *
4
+ * A single small decision procedure for failures: retry, repair, expand
5
+ * context, change hypothesis, replan, escalate, or stop. No new "Recovery
6
+ * Brain"; just one policy.
7
+ */
8
+
9
+ export function decideRecovery({
10
+ failure = {},
11
+ attemptCount = 1,
12
+ sameFailureCount = 0,
13
+ hasContextGap = false,
14
+ maxAttempts = 3,
15
+ budgetRemaining = true,
16
+ } = {}) {
17
+ if (!budgetRemaining) return { action: 'stop', reason: 'budget exhausted' }
18
+ if (attemptCount >= maxAttempts) return { action: 'escalate', reason: `max attempts (${maxAttempts}) reached` }
19
+ if (sameFailureCount >= 2) {
20
+ return {
21
+ action: hasContextGap ? 'expand_context' : 'change_hypothesis',
22
+ reason: `same failure repeated ${sameFailureCount} times`,
23
+ }
24
+ }
25
+ if (hasContextGap) return { action: 'expand_context', reason: 'missing context for root cause' }
26
+ if (failure.category === 'test_failure' || failure.category === 'build_failure') {
27
+ return { action: 'repair', reason: 'diagnose and repair with evidence' }
28
+ }
29
+ return { action: 'retry', reason: 'transient or unknown failure, retry once' }
30
+ }
31
+
32
+ export function formatRecoveryDecision(decision = {}) {
33
+ return `${decision.action} — ${decision.reason || ''}`
34
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * TaskContract.
3
+ *
4
+ * The single immutable contract that all Omni modules consume after Decide.
5
+ * It replaces ad-hoc re-classification across Planner/Runtime/Verifier.
6
+ */
7
+
8
+ import { decideIntelligenceLevel } from './progressive-intelligence.mjs'
9
+
10
+ export function buildTaskContract({
11
+ taskText = '',
12
+ decision = {},
13
+ acceptance = [],
14
+ constraints = [],
15
+ nonGoals = [],
16
+ requiredCapabilities = [],
17
+ } = {}) {
18
+ const intelligence = decideIntelligenceLevel(decision)
19
+ return {
20
+ objective: taskText,
21
+ constraints: Array.isArray(constraints) ? constraints : [],
22
+ nonGoals: Array.isArray(nonGoals) ? nonGoals : [],
23
+ acceptance: Array.isArray(acceptance) && acceptance.length ? acceptance : [`${taskText} is complete`],
24
+ risk: decision.risk || 'low',
25
+ uncertainty: decision.uncertainty ?? 0.1,
26
+ intelligenceLevel: intelligence.level,
27
+ contextBudget: decision.contextBudget || 20000,
28
+ verificationPolicy: {
29
+ level: intelligence.verification,
30
+ independentVerify: intelligence.independentVerify,
31
+ approvalRequired: intelligence.approvalRequired,
32
+ },
33
+ requiredCapabilities: Array.isArray(requiredCapabilities) && requiredCapabilities.length
34
+ ? requiredCapabilities
35
+ : (decision.evidenceRequirements || []),
36
+ reasoningEffort: intelligence.reasoningEffort,
37
+ }
38
+ }
39
+
40
+ export function formatTaskContract(contract = {}) {
41
+ const lines = [
42
+ `Objective: ${contract.objective || ''}`,
43
+ `Risk: ${contract.risk} | Uncertainty: ${contract.uncertainty}`,
44
+ `Intelligence: ${contract.intelligenceLevel}`,
45
+ `Verification: ${contract.verificationPolicy?.level}${contract.verificationPolicy?.independentVerify ? ' + independent' : ''}${contract.verificationPolicy?.approvalRequired ? ' + approval' : ''}`,
46
+ `Context budget: ${contract.contextBudget}`,
47
+ ]
48
+ if (contract.acceptance?.length) lines.push(`Acceptance:\n${contract.acceptance.map((c) => `- ${c}`).join('\n')}`)
49
+ if (contract.requiredCapabilities?.length) lines.push(`Required capabilities: ${contract.requiredCapabilities.join(', ')}`)
50
+ return lines.join('\n')
51
+ }
@@ -0,0 +1,33 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ attachFingerprint,
6
+ fingerprintFiles,
7
+ fingerprintText,
8
+ fingerprintWorkspace,
9
+ isContextStale,
10
+ } from '../src/context-freshness.mjs'
11
+
12
+ test('fingerprintText is deterministic', () => {
13
+ assert.equal(fingerprintText('abc'), fingerprintText('abc'))
14
+ assert.notEqual(fingerprintText('abc'), fingerprintText('abd'))
15
+ })
16
+
17
+ test('fingerprintFiles changes when content changes', () => {
18
+ const a = fingerprintFiles({ 'a.ts': 'x' })
19
+ const b = fingerprintFiles({ 'a.ts': 'y' })
20
+ assert.notEqual(a, b)
21
+ })
22
+
23
+ test('fingerprintWorkspace combines commit and files', () => {
24
+ const fp = fingerprintWorkspace({ commit: 'abc', files: { 'a.ts': 'x' } })
25
+ assert.ok(fp.length > 0)
26
+ })
27
+
28
+ test('attachFingerprint and isContextStale', () => {
29
+ const context = attachFingerprint({ files: [] }, 'fp1')
30
+ assert.equal(context.workspaceFingerprint, 'fp1')
31
+ assert.equal(isContextStale(context, 'fp1'), false)
32
+ assert.equal(isContextStale(context, 'fp2'), true)
33
+ })
@@ -0,0 +1,52 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ TRUST_LEVELS,
6
+ createEvidenceRecord,
7
+ evidenceMeetsTrust,
8
+ formatEvidenceTrust,
9
+ invalidateEvidence,
10
+ isEvidenceStale,
11
+ requiredTrustForRisk,
12
+ } from '../src/evidence-trust.mjs'
13
+
14
+ test('TRUST_LEVELS are ordered', () => {
15
+ assert.ok(TRUST_LEVELS.T3 > TRUST_LEVELS.T1)
16
+ assert.equal(TRUST_LEVELS.T4, 4)
17
+ })
18
+
19
+ test('createEvidenceRecord includes fingerprint and trust', () => {
20
+ const record = createEvidenceRecord({ criterionId: 'c1', workspaceFingerprint: 'fp1', trustLevel: 'T3' })
21
+ assert.equal(record.trustValue, 3)
22
+ assert.equal(record.criterionId, 'c1')
23
+ })
24
+
25
+ test('isEvidenceStale detects fingerprint mismatch', () => {
26
+ const record = createEvidenceRecord({ workspaceFingerprint: 'fp1' })
27
+ assert.equal(isEvidenceStale(record, 'fp1'), false)
28
+ assert.equal(isEvidenceStale(record, 'fp2'), true)
29
+ })
30
+
31
+ test('invalidateEvidence marks stale records', () => {
32
+ const records = [
33
+ createEvidenceRecord({ workspaceFingerprint: 'fp1' }),
34
+ createEvidenceRecord({ workspaceFingerprint: 'fp2' }),
35
+ ]
36
+ const invalidated = invalidateEvidence(records, 'fp2')
37
+ assert.equal(invalidated[0].stale, true)
38
+ assert.equal(invalidated[1].stale, false)
39
+ })
40
+
41
+ test('requiredTrustForRisk escalates for high risk', () => {
42
+ assert.equal(requiredTrustForRisk('low').minimum, 'T1')
43
+ assert.equal(requiredTrustForRisk('high').minimum, 'T3')
44
+ assert.equal(requiredTrustForRisk('high').independent, true)
45
+ })
46
+
47
+ test('evidenceMeetsTrust compares trust values', () => {
48
+ const record = createEvidenceRecord({ trustLevel: 'T3' })
49
+ assert.equal(evidenceMeetsTrust(record, 'T2'), true)
50
+ assert.equal(evidenceMeetsTrust(record, 'T4'), false)
51
+ assert.match(formatEvidenceTrust(record), /T3/)
52
+ })
@@ -0,0 +1,31 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ createHostAdapter,
6
+ describeHostCapabilities,
7
+ formatHostNegotiation,
8
+ negotiateHost,
9
+ } from '../src/host-interface.mjs'
10
+
11
+ test('createHostAdapter validates describeHost', () => {
12
+ const valid = createHostAdapter({ describeHost: async () => ({}) })
13
+ assert.equal(valid.valid, true)
14
+ const invalid = createHostAdapter({})
15
+ assert.equal(invalid.valid, false)
16
+ assert.ok(invalid.missing.includes('describeHost'))
17
+ })
18
+
19
+ test('describeHostCapabilities defaults to false', () => {
20
+ const caps = describeHostCapabilities({ workflow: true })
21
+ assert.equal(caps.workflow, true)
22
+ assert.equal(caps.skills, false)
23
+ })
24
+
25
+ test('negotiateHost reports supported and degraded features', () => {
26
+ const result = negotiateHost({ workflow: true, approvals: true, skills: false, plugins: false })
27
+ assert.ok(result.supported.includes('workflow'))
28
+ assert.ok(result.degraded.includes('skills'))
29
+ assert.equal(result.mode, 'degraded')
30
+ assert.match(formatHostNegotiation(result), /Host mode: degraded/)
31
+ })
@@ -0,0 +1,44 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ decideIntervention,
6
+ expectedInterventionUtility,
7
+ formatInterventionGate,
8
+ interventionEfficiency,
9
+ noOpPrecision,
10
+ shouldIntervene,
11
+ } from '../src/intervention-gate.mjs'
12
+
13
+ test('expectedInterventionUtility favors high gain low overhead', () => {
14
+ const high = expectedInterventionUtility({ successGain: 0.2, tokenOverhead: 0.05 })
15
+ const low = expectedInterventionUtility({ successGain: 0.01, tokenOverhead: 0.8 })
16
+ assert.ok(high > low)
17
+ })
18
+
19
+ test('shouldIntervene respects threshold', () => {
20
+ assert.equal(shouldIntervene(0.2, 0.1), true)
21
+ assert.equal(shouldIntervene(0.05, 0.1), false)
22
+ })
23
+
24
+ test('decideIntervention returns NOOP by default for low utility', () => {
25
+ const result = decideIntervention({ successGain: 0, tokenOverhead: 0.5 })
26
+ assert.equal(result.intervene, false)
27
+ assert.equal(result.mode, 'noop')
28
+ assert.match(formatInterventionGate(result), /NOOP/)
29
+ })
30
+
31
+ test('noOpPrecision measures correct no-op calls', () => {
32
+ const precision = noOpPrecision([
33
+ { predictedNoop: true, actuallyNeeded: false },
34
+ { predictedNoop: true, actuallyNeeded: true },
35
+ { predictedNoop: false, actuallyNeeded: false },
36
+ ])
37
+ assert.equal(precision, 0.5)
38
+ })
39
+
40
+ test('interventionEfficiency computes gain per overhead', () => {
41
+ const result = interventionEfficiency({ rawSuccess: 0.6, omniSuccess: 0.78, tokenOverhead: 0.3 })
42
+ assert.equal(result.gain, 0.18)
43
+ assert.equal(result.efficiency, 0.6)
44
+ })
@@ -0,0 +1,35 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ OMNI_EVENT_TYPES,
6
+ createOmniEvent,
7
+ normalizeHostEvent,
8
+ } from '../src/omni-event.mjs'
9
+
10
+ test('createOmniEvent builds host-independent event', () => {
11
+ const event = createOmniEvent({ type: 'test.completed', host: 'dsh', sessionId: 's1', taskId: 't1', workspaceFingerprint: 'fp', payload: { passed: 10 } })
12
+ assert.equal(event.type, 'test.completed')
13
+ assert.equal(event.host, 'dsh')
14
+ assert.equal(event.workspaceFingerprint, 'fp')
15
+ assert.ok(event.timestamp)
16
+ })
17
+
18
+ test('normalizeHostEvent maps DSH tool events', () => {
19
+ const event = normalizeHostEvent({ type: 'tool.started', name: 'edit', data: { file: 'a.ts' } }, 'dsh', { sessionId: 's', taskId: 't' })
20
+ assert.equal(event.type, 'tool.started')
21
+ assert.equal(event.sessionId, 's')
22
+ assert.equal(event.payload.file, 'a.ts')
23
+ })
24
+
25
+ test('normalizeHostEvent maps generic start/end to model events', () => {
26
+ const started = normalizeHostEvent({ type: 'start' }, 'codex')
27
+ const ended = normalizeHostEvent({ type: 'finished' }, 'codex')
28
+ assert.equal(started.type, 'model.started')
29
+ assert.equal(ended.type, 'model.completed')
30
+ })
31
+
32
+ test('OMNI_EVENT_TYPES is stable', () => {
33
+ assert.ok(OMNI_EVENT_TYPES.includes('file.changed'))
34
+ assert.ok(OMNI_EVENT_TYPES.includes('approval.completed'))
35
+ })
@@ -0,0 +1,33 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ decideRecovery,
6
+ formatRecoveryDecision,
7
+ } from '../src/recovery-policy.mjs'
8
+
9
+ test('decideRecovery repairs test/build failures', () => {
10
+ const decision = decideRecovery({ failure: { category: 'test_failure' }, attemptCount: 1 })
11
+ assert.equal(decision.action, 'repair')
12
+ })
13
+
14
+ test('decideRecovery expands context when gap exists', () => {
15
+ const decision = decideRecovery({ failure: {}, hasContextGap: true })
16
+ assert.equal(decision.action, 'expand_context')
17
+ })
18
+
19
+ test('decideRecovery changes hypothesis after repeated same failure', () => {
20
+ const decision = decideRecovery({ failure: { category: 'test_failure' }, sameFailureCount: 2, hasContextGap: false })
21
+ assert.equal(decision.action, 'change_hypothesis')
22
+ })
23
+
24
+ test('decideRecovery escalates after max attempts', () => {
25
+ const decision = decideRecovery({ attemptCount: 4, maxAttempts: 3 })
26
+ assert.equal(decision.action, 'escalate')
27
+ })
28
+
29
+ test('decideRecovery stops when budget exhausted', () => {
30
+ const decision = decideRecovery({ budgetRemaining: false })
31
+ assert.equal(decision.action, 'stop')
32
+ assert.match(formatRecoveryDecision(decision), /stop/)
33
+ })
@@ -0,0 +1,33 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ buildTaskContract,
6
+ formatTaskContract,
7
+ } from '../src/task-contract.mjs'
8
+
9
+ test('buildTaskContract produces a single unified contract', () => {
10
+ const contract = buildTaskContract({
11
+ taskText: 'Fix payment race',
12
+ decision: { complexity: 'plan', risk: 'high', type: 'bugfix' },
13
+ acceptance: ['no double charge', 'tests pass'],
14
+ requiredCapabilities: ['debugging', 'verification'],
15
+ })
16
+ assert.equal(contract.objective, 'Fix payment race')
17
+ assert.equal(contract.intelligenceLevel, 'L3')
18
+ assert.equal(contract.verificationPolicy.independentVerify, true)
19
+ assert.deepEqual(contract.acceptance, ['no double charge', 'tests pass'])
20
+ assert.ok(contract.requiredCapabilities.includes('verification'))
21
+ })
22
+
23
+ test('buildTaskContract fills default acceptance', () => {
24
+ const contract = buildTaskContract({ taskText: 'Fix typo' })
25
+ assert.equal(contract.acceptance.length, 1)
26
+ assert.match(contract.acceptance[0], /Fix typo/)
27
+ })
28
+
29
+ test('formatTaskContract renders readable summary', () => {
30
+ const text = formatTaskContract(buildTaskContract({ taskText: 'x' }))
31
+ assert.match(text, /Objective: x/)
32
+ assert.match(text, /Intelligence: L0/)
33
+ })