dsh-omni-router 2.2.1 → 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
@@ -3,6 +3,10 @@
3
3
  > GitHub: https://github.com/qwe225380/dsh-omni-router
4
4
  > [中文说明](./README.zh-CN.md)
5
5
 
6
+ > **Fast by default. Smart when needed. Proven when done.**
7
+ > **Decide · Compose · Verify**
8
+ > Install it. Keep coding.
9
+
6
10
  **Omni** 是一个 DeepSeek Harness 的 **reliability and orchestration control plane**,定位为 Intelligence Runtime:把复杂度路由、任务编译、项目上下文、能力编排、Mission DAG、Runtime 预算、证据验证、Memory/Learning、Benchmark 组合成一个统一的、自动运行的工程闭环。
7
11
 
8
12
  ## Why Omni Router?
@@ -74,6 +78,8 @@
74
78
  - **P1/P3 能力**:Real capability sandbox(已接入 `omni_mission_run`,按 role + capability 生成 toolFilter)、Capability Resolver v2(risk/reliability 综合打分)、OmniBench v2 runner(manifest schema + prepare + prompt generation + `--exec` 本地执行/结果采集 + DAG-to-plan compile)。
75
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。
76
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
+ - **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)。
77
83
  - **TaskDecision**:`createTaskDecision` 生成唯一决策对象,Policy/Runtime 统一消费,避免重复 classify 分叉。
78
84
  - **Evidence Protocol**:`src/evidence.mjs` 用结构化 command/file/test/finding 证据判定 PASS/FAIL;`isQaPass` 已支持从 QA 输出解析 JSON evidence,不再只信文本。
79
85
  - **Task Compiler**:`compileTask` 生成 objective / constraints / non-goals / acceptance / hidden assumptions / ambiguities / invariants / risk / artifacts;`compileTaskWithLLM` 可用 LLM 增强。
@@ -155,7 +161,7 @@ Then restart DSH and select **Omni Router** in a new session.
155
161
  - `/omni direct` — enter direct mode.
156
162
  - `/omni mode spec|react|balanced` — set thinking mode.
157
163
  - `/omni reroute plan|direct` — adaptively reroute current task.
158
- - Model tools: `omni_status`, `omni_plan`, `omni_direct`, `omni_mode`, `omni_reroute`, `omni_delegate`, `omni_memory`, `omni_benchmark`, `omni_mission_run`, `omni_mission_resume`, `omni_capability_audit`, `omni_capability_provision`, `omni_capability_probe`, `omni_capability_performance`, `omni_visual_check`.
164
+ - Model tools: `omni_status`, `omni_explain`, `omni_doctor`, `omni_plan`, `omni_direct`, `omni_mode`, `omni_reroute`, `omni_delegate`, `omni_memory`, `omni_benchmark`, `omni_mission_run`, `omni_mission_resume`, `omni_capability_audit`, `omni_capability_provision`, `omni_capability_probe`, `omni_capability_performance`, `omni_visual_check`.
159
165
 
160
166
  ## Configuration
161
167
 
package/README.zh-CN.md CHANGED
@@ -3,6 +3,10 @@
3
3
  > GitHub:https://github.com/qwe225380/dsh-omni-router
4
4
  > [English](./README.md)
5
5
 
6
+ > **默认快,必要时聪明,完成必须有证据。**
7
+ > **Decide · Compose · Verify**
8
+ > 安装即可使用,继续写代码。
9
+
6
10
  **Omni** 是一个 DeepSeek Harness 的 **reliability and orchestration control plane**,定位为 Intelligence Runtime:把复杂度路由、任务编译、项目上下文、能力编排、Mission DAG、Runtime 预算、证据验证、Memory/Learning、Benchmark 组合成一个统一的、自动运行的工程闭环。
7
11
 
8
12
  ## 为什么需要 Omni Router?
@@ -74,6 +78,8 @@
74
78
  - **P1/P3 能力**:Real capability sandbox(已接入 `omni_mission_run`,按 role + capability 生成 toolFilter)、Capability Resolver v2(risk/reliability 综合打分)、OmniBench v2 runner(manifest schema + prepare + prompt generation + `--exec` 本地执行/结果采集 + DAG-to-plan compile)。
75
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。
76
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
+ - **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)。
77
83
  - **TaskDecision**:`createTaskDecision` 生成唯一决策对象,Policy/Runtime 统一消费,避免重复 classify 分叉。
78
84
  - **Evidence Protocol**:`src/evidence.mjs` 用结构化 command/file/test/finding 证据判定 PASS/FAIL;`isQaPass` 已支持从 QA 输出解析 JSON evidence,不再只信文本。
79
85
  - **Task Compiler**:`compileTask` 生成 objective / constraints / non-goals / acceptance / hidden assumptions / ambiguities / invariants / risk / artifacts;`compileTaskWithLLM` 可用 LLM 增强。
@@ -155,7 +161,7 @@ node scripts/install-preset.mjs
155
161
  - `/omni direct` — 进入直接执行模式。
156
162
  - `/omni mode spec|react|balanced` — 设置思维模式。
157
163
  - `/omni reroute plan|direct` — 动态切换当前任务路由。
158
- - 模型工具:`omni_status` / `omni_plan` / `omni_direct` / `omni_mode` / `omni_reroute` / `omni_delegate` / `omni_memory` / `omni_benchmark` / `omni_mission_run` / `omni_mission_resume` / `omni_capability_audit` / `omni_capability_provision` / `omni_capability_probe` / `omni_capability_performance` / `omni_visual_check`。
164
+ - 模型工具:`omni_status` / `omni_explain` / `omni_doctor` / `omni_plan` / `omni_direct` / `omni_mode` / `omni_reroute` / `omni_delegate` / `omni_memory` / `omni_benchmark` / `omni_mission_run` / `omni_mission_resume` / `omni_capability_audit` / `omni_capability_provision` / `omni_capability_probe` / `omni_capability_performance` / `omni_visual_check`。
159
165
 
160
166
  ## 配置
161
167
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-omni-router",
3
- "version": "2.2.1",
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/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",
@@ -11,6 +11,7 @@
11
11
 
12
12
  import { buildPhaseTasks, decideReplan } from './mission-planner.mjs'
13
13
  import { applyObservationToDag, getReadyTasks, isMissionDagComplete, markTaskDone, selectReadyBatch } from './mission-dag.mjs'
14
+ import { nextStrategy, shouldShiftStrategy } from './strategy-shift.mjs'
14
15
 
15
16
  export function createRuntimeState(mission, options = {}) {
16
17
  const phases = mission?.phases || []
@@ -187,6 +188,7 @@ export async function runDagLoop(dag, {
187
188
  let cost = 0
188
189
  let toolCalls = 0
189
190
  const actions = []
191
+ const failureHistory = new Map()
190
192
  const startTime = Date.now()
191
193
 
192
194
  const statusFromBudget = () => {
@@ -219,12 +221,21 @@ export async function runDagLoop(dag, {
219
221
  tokenUsage += Number(result?.tokenUsage || 0)
220
222
  cost += Number(result?.cost || 0)
221
223
  toolCalls += Number(result?.toolCalls || 0)
222
- const isFailure = observation?.type === 'test_failure' || observation?.type === 'build_failure'
224
+ const isFailure = observation?.type === 'test_failure' || observation?.type === 'build_failure' || observation?.type === 'strategy_shift'
223
225
  if (isFailure) {
224
226
  replanCount += 1
225
227
  repairCount += 1
226
228
  sameActionCount = sameActionCount + 1
227
- current = applyObservationToDag(current, { ...observation, taskId: task.id }, task.id)
229
+ const logicalKey = task.logicalId || task.id
230
+ const history = failureHistory.get(logicalKey) || []
231
+ history.push({ category: observation.type, reason: observation.reason, hypothesis: observation.hypothesis })
232
+ failureHistory.set(logicalKey, history)
233
+ let appliedObservation = { ...observation, taskId: task.id }
234
+ if (shouldShiftStrategy(history)) {
235
+ appliedObservation = { type: 'strategy_shift', reason: nextStrategy(history), taskId: task.id }
236
+ failureHistory.delete(logicalKey)
237
+ }
238
+ current = applyObservationToDag(current, appliedObservation, task.id)
228
239
  } else {
229
240
  sameActionCount = 0
230
241
  current = markTaskDone(current, task.id, observation)
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Context Capsule.
3
+ *
4
+ * Gives a weak model the smallest useful slice of context for the current
5
+ * task: a few relevant files, their callers/callees, related tests, invariants,
6
+ * and recent failures. This is the opposite of dumping the whole repository
7
+ * map into the prompt.
8
+ */
9
+
10
+ const STOP_WORDS = new Set([
11
+ 'the', 'and', 'for', 'with', 'that', 'this', 'from', 'are', 'was', 'were',
12
+ 'not', 'but', 'you', 'your', 'fix', 'add', 'make', 'use', 'should', 'when',
13
+ ])
14
+
15
+ export function buildContextCapsule(taskText, options = {}) {
16
+ const entries = options.entries || []
17
+ const files = options.files || {}
18
+ const graph = options.graph || {}
19
+ const needs = options.needs || []
20
+ const invariants = options.invariants || []
21
+ const failures = options.failures || []
22
+ const maxFiles = Number(options.maxFiles) || 6
23
+ const maxTotalChars = Number(options.maxTotalChars) || 8000
24
+ const maxFileChars = Number(options.maxFileChars) || 1200
25
+
26
+ const keywords = extractKeywords([taskText, ...needs])
27
+ const fileNames = entries.filter((e) => e.type === 'file').map((e) => e.name)
28
+ const scored = fileNames.map((name) => {
29
+ const content = files[name] || ''
30
+ const nameScore = scoreText(name, keywords)
31
+ const contentScore = content ? scoreText(content.slice(0, 2000), keywords) * 0.5 : 0
32
+ return { name, score: nameScore + contentScore }
33
+ }).sort((a, b) => b.score - a.score)
34
+ const selected = scored.slice(0, maxFiles).map((s) => s.name)
35
+
36
+ const lines = []
37
+ lines.push(`Task: ${taskText}`)
38
+ if (needs.length) lines.push(`Needs: ${needs.join('; ')}`)
39
+ if (selected.length) {
40
+ lines.push('', 'Relevant files:')
41
+ for (const name of selected) {
42
+ const content = files[name] || ''
43
+ lines.push(`--- ${name} ---`)
44
+ lines.push(content ? content.slice(0, maxFileChars) : '(no content indexed)')
45
+ }
46
+ }
47
+
48
+ const edgeLines = []
49
+ for (const name of selected) {
50
+ for (const edge of graph[name] || []) {
51
+ edgeLines.push(`${name} ${edge.kind || '->'} ${edge.to || edge}`)
52
+ }
53
+ }
54
+ if (edgeLines.length) {
55
+ lines.push('', 'Callers / callees:')
56
+ lines.push(...edgeLines.slice(0, 20))
57
+ }
58
+
59
+ const tests = fileNames.filter((n) =>
60
+ /\.(test|spec)\./i.test(n) &&
61
+ (selected.includes(n) || keywords.some((k) => n.toLowerCase().includes(k))),
62
+ )
63
+ if (tests.length) {
64
+ lines.push('', 'Tests:')
65
+ lines.push(...tests.map((t) => `- ${t}`))
66
+ }
67
+
68
+ if (invariants.length) {
69
+ lines.push('', 'Invariants:')
70
+ lines.push(...invariants.map((i) => `- ${i}`))
71
+ }
72
+
73
+ if (failures.length) {
74
+ lines.push('', 'Recent failures:')
75
+ lines.push(...failures.map((f) => `- ${f}`))
76
+ }
77
+
78
+ let text = lines.join('\n')
79
+ if (text.length > maxTotalChars) {
80
+ text = `${text.slice(0, maxTotalChars)}\n… (context capsule truncated)`
81
+ }
82
+ return text
83
+ }
84
+
85
+ export function expandContextCapsule(current, request, options = {}) {
86
+ const requestText = Array.isArray(request) ? request.join('; ') : String(request || '')
87
+ if (!requestText) return current
88
+ const extra = buildContextCapsule(requestText, {
89
+ ...options,
90
+ maxFiles: Math.max(Number(options.maxFiles) || 6, 3),
91
+ maxTotalChars: Math.max(Number(options.maxTotalChars) || 8000, 4000),
92
+ })
93
+ return `${current}\n\n--- additional context requested ---\n${extra}`
94
+ }
95
+
96
+ function extractKeywords(texts = []) {
97
+ const words = new Set()
98
+ for (const text of texts) {
99
+ for (const match of String(text || '').toLowerCase().match(/[a-z0-9_]+/g) || []) {
100
+ if (match.length > 2 && !STOP_WORDS.has(match)) words.add(match)
101
+ }
102
+ }
103
+ return [...words]
104
+ }
105
+
106
+ function scoreText(text, keywords) {
107
+ const lower = String(text || '').toLowerCase()
108
+ let score = 0
109
+ for (const keyword of keywords) {
110
+ if (lower.includes(keyword)) score += 1
111
+ }
112
+ return score
113
+ }
@@ -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
+ }
@@ -103,7 +103,7 @@ export function insertAfter(dag, afterId, task) {
103
103
 
104
104
  export function applyObservationToDag(dag, observation = {}, failedTaskId = null) {
105
105
  const type = observation.type || ''
106
- if (type === 'test_failure' || type === 'build_failure') {
106
+ if (type === 'test_failure' || type === 'build_failure' || type === 'strategy_shift') {
107
107
  const taskId = failedTaskId || observation.taskId || null
108
108
  const failedTask = taskId ? dag.tasks.find((t) => t.id === taskId) : null
109
109
  const baseId = taskId || 'T'
@@ -114,13 +114,16 @@ export function applyObservationToDag(dag, observation = {}, failedTaskId = null
114
114
  const tasks = (dag.tasks || []).map((t) => t.id === taskId ? { ...t, status: 'failed', attempt, failure: observation } : t)
115
115
  const doneIds = tasks.filter((t) => t.status === 'done').map((t) => t.id)
116
116
  const failure = classifyFailure(observation)
117
+ const isShift = type === 'strategy_shift'
117
118
  const repair = createTask({
118
119
  id: repairId,
119
- goal: `Diagnose and repair: ${failure.category} — ${failure.recovery}`,
120
+ goal: isShift
121
+ ? `Strategy shift: ${observation.reason || 're-investigate with broader context and fresh capabilities'}`
122
+ : `Diagnose and repair: ${failure.category} — ${failure.recovery}`,
120
123
  dependencies: doneIds,
121
- acceptance: ['verification passes after repair'],
124
+ acceptance: ['root cause is re-diagnosed and verification passes'],
122
125
  verification: ['run failing checks again'],
123
- requiredCapabilities: ['debugging'],
126
+ requiredCapabilities: isShift ? ['debugging', 'repository.search'] : ['debugging'],
124
127
  forbiddenCapabilities: ['source.write'],
125
128
  })
126
129
  // Repair tasks should be allowed to write; fix forbidden.
@@ -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
+ }