dsh-omni-router 2.2.0 → 2.3.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,7 @@
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` 用户入口。
77
82
  - **TaskDecision**:`createTaskDecision` 生成唯一决策对象,Policy/Runtime 统一消费,避免重复 classify 分叉。
78
83
  - **Evidence Protocol**:`src/evidence.mjs` 用结构化 command/file/test/finding 证据判定 PASS/FAIL;`isQaPass` 已支持从 QA 输出解析 JSON evidence,不再只信文本。
79
84
  - **Task Compiler**:`compileTask` 生成 objective / constraints / non-goals / acceptance / hidden assumptions / ambiguities / invariants / risk / artifacts;`compileTaskWithLLM` 可用 LLM 增强。
@@ -155,7 +160,7 @@ Then restart DSH and select **Omni Router** in a new session.
155
160
  - `/omni direct` — enter direct mode.
156
161
  - `/omni mode spec|react|balanced` — set thinking mode.
157
162
  - `/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_visual_check`.
163
+ - 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
164
 
160
165
  ## Configuration
161
166
 
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,7 @@
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` 用户入口。
77
82
  - **TaskDecision**:`createTaskDecision` 生成唯一决策对象,Policy/Runtime 统一消费,避免重复 classify 分叉。
78
83
  - **Evidence Protocol**:`src/evidence.mjs` 用结构化 command/file/test/finding 证据判定 PASS/FAIL;`isQaPass` 已支持从 QA 输出解析 JSON evidence,不再只信文本。
79
84
  - **Task Compiler**:`compileTask` 生成 objective / constraints / non-goals / acceptance / hidden assumptions / ambiguities / invariants / risk / artifacts;`compileTaskWithLLM` 可用 LLM 增强。
@@ -155,7 +160,7 @@ node scripts/install-preset.mjs
155
160
  - `/omni direct` — 进入直接执行模式。
156
161
  - `/omni mode spec|react|balanced` — 设置思维模式。
157
162
  - `/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_visual_check`。
163
+ - 模型工具:`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
164
 
160
165
  ## 配置
161
166
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-omni-router",
3
- "version": "2.2.0",
3
+ "version": "2.3.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/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)
@@ -6,12 +6,36 @@
6
6
  * demotion/removal decisions and benchmark plugin-contribution analysis.
7
7
  */
8
8
 
9
+ import fs from 'node:fs'
10
+ import path from 'node:path'
11
+
9
12
  export function createPerformanceRegistry(initial = {}) {
10
13
  return {
11
14
  providers: { ...(initial.providers || {}) },
12
15
  }
13
16
  }
14
17
 
18
+ export function performanceRegistryPath(cwd) {
19
+ return path.join(cwd, '.omni', 'capability-performance.json')
20
+ }
21
+
22
+ export function loadPerformanceRegistry(cwd) {
23
+ const file = performanceRegistryPath(cwd)
24
+ if (!fs.existsSync(file)) return createPerformanceRegistry()
25
+ try {
26
+ return createPerformanceRegistry(JSON.parse(fs.readFileSync(file, 'utf8')))
27
+ } catch {
28
+ return createPerformanceRegistry()
29
+ }
30
+ }
31
+
32
+ export function savePerformanceRegistry(cwd, registry) {
33
+ const file = performanceRegistryPath(cwd)
34
+ fs.mkdirSync(path.dirname(file), { recursive: true })
35
+ fs.writeFileSync(file, JSON.stringify(registry, null, 2), 'utf8')
36
+ return file
37
+ }
38
+
15
39
  export function recordProvisionOutcome(registry, providerId, outcome = {}) {
16
40
  const previous = registry.providers[providerId] || {
17
41
  runs: 0,
@@ -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
+ }
@@ -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.
@@ -37,6 +37,8 @@ import { loadCapabilityManifests } from './capability-manifest.mjs'
37
37
  import { capabilityToolFilter } from './capability-sandbox.mjs'
38
38
  import { baselineAudit, formatCapabilityAudit, taskTimeAudit } from './capability-auditor.mjs'
39
39
  import { createStaticRegistryAdapter, discoverCandidates, evaluateProvisionPlan, formatProvisionResult, probeCapability, provisionCapabilities } from './capability-provisioner.mjs'
40
+ import { evaluateProviderValue, formatPerformanceRegistry, loadPerformanceRegistry, recommendDemotion, recordProvisionOutcome, savePerformanceRegistry } from './capability-performance.mjs'
41
+ import { decideIntelligenceLevel, formatIntelligenceLevel } from './progressive-intelligence.mjs'
40
42
  import { buildProgressiveContext } from './context-expansion.mjs'
41
43
  import { buildDynamicContext } from './dynamic-context.mjs'
42
44
  import { classifyFailure } from './failure-taxonomy.mjs'
@@ -1010,11 +1012,20 @@ export function apply(ctx, config = {}) {
1010
1012
  const agent = agentFor(session)
1011
1013
  const state = states.get(session.id) || { kind: null, taskType: null, thinkingMode: null, riskLevel: null, firstText: null, planRequested: false, directOverride: false }
1012
1014
  const routerStandard = agent ? isRouterStandardAvailable(ctx.get('tools') || ctx.tools, agent) : false
1015
+ const taskDecision = createTaskDecision({
1016
+ taskText: state.firstText || '',
1017
+ taskType: state.taskType || 'other',
1018
+ complexity: state.kind === 'plan' ? 'plan' : state.kind === 'direct' ? 'direct' : 'balanced',
1019
+ risk: state.riskLevel || 'low',
1020
+ thinkingMode: state.thinkingMode || 'balanced',
1021
+ })
1022
+ const intelligence = decideIntelligenceLevel(taskDecision)
1013
1023
  return [
1014
1024
  `omni-router: ${state.kind || 'unclassified'}`,
1015
1025
  `taskType=${state.taskType || 'unknown'}`,
1016
1026
  `thinkingMode=${state.thinkingMode || 'balanced'}`,
1017
1027
  `riskLevel=${state.riskLevel || 'unknown'}`,
1028
+ `intelligenceLevel=${formatIntelligenceLevel(intelligence)}`,
1018
1029
  `planRequested=${state.planRequested}`,
1019
1030
  `directOverride=${state.directOverride}`,
1020
1031
  `routerStandard=${routerStandard ? 'delegated' : 'not-detected'}`,
@@ -1022,6 +1033,69 @@ export function apply(ctx, config = {}) {
1022
1033
  },
1023
1034
  })
1024
1035
 
1036
+ registerTool({
1037
+ name: 'omni_explain',
1038
+ description: 'Explain why Omni chose the current mode, capabilities, verification level, and what is needed for completion.',
1039
+ parameters: {
1040
+ type: 'object',
1041
+ properties: {
1042
+ topic: { type: 'string', description: 'Optional topic to focus on: mode, capabilities, verification, completion' },
1043
+ },
1044
+ },
1045
+ execute(args) {
1046
+ const session = currentSession()
1047
+ if (!session) return 'no agent session'
1048
+ const state = states.get(session.id) || {}
1049
+ const decision = createTaskDecision({
1050
+ taskText: state.firstText || '',
1051
+ taskType: state.taskType || 'other',
1052
+ complexity: state.kind === 'plan' ? 'plan' : state.kind === 'direct' ? 'direct' : 'balanced',
1053
+ risk: state.riskLevel || 'low',
1054
+ thinkingMode: state.thinkingMode || 'balanced',
1055
+ })
1056
+ const intelligence = decideIntelligenceLevel(decision)
1057
+ const topic = String(args?.topic || '').toLowerCase()
1058
+ const lines = [
1059
+ `Mode: ${intelligence.level} ${intelligence.label}`,
1060
+ `Why: complexity=${decision.complexity}, risk=${decision.risk}, taskType=${decision.type}`,
1061
+ `Reasoning effort: ${intelligence.reasoningEffort}`,
1062
+ `Verification: ${intelligence.verification}`,
1063
+ `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'}`,
1066
+ ]
1067
+ if (topic === 'mode' || topic === 'capabilities' || topic === 'verification' || topic === 'completion') {
1068
+ const idx = lines.findIndex((l) => l.toLowerCase().startsWith(topic))
1069
+ return idx >= 0 ? lines[idx] : lines.join('\n')
1070
+ }
1071
+ return lines.join('\n')
1072
+ },
1073
+ })
1074
+
1075
+ registerTool({
1076
+ name: 'omni_doctor',
1077
+ description: 'Run a quick environment/capability/project-index health check.',
1078
+ parameters: {},
1079
+ async execute() {
1080
+ const session = currentSession()
1081
+ const toolsService = ctx.get('tools') || ctx.tools
1082
+ const toolNames = await collectToolNames(toolsService)
1083
+ let brain = autoPopulateCapabilityBrain(createCapabilityBrain(), toolNames)
1084
+ brain = loadCapabilityManifests(brain, config.capabilityManifests || [])
1085
+ const baseline = baselineAudit(brain)
1086
+ const fs = ctx.get('fs') || ctx.fs
1087
+ const lines = [
1088
+ `DSH session: ${session ? 'ok' : 'missing'}`,
1089
+ `Tools registered: ${toolNames.length}`,
1090
+ `Baseline capability coverage: ${baseline.available.length}/${baseline.required.length}`,
1091
+ `Missing baseline: ${baseline.missing.join(', ') || '(none)'}`,
1092
+ `ProjectIndex: ${fs ? 'available' : 'unavailable'}`,
1093
+ `Evidence hooks: ${ctx.get('evidence') || ctx.evidence ? 'available' : 'not-detected'}`,
1094
+ ]
1095
+ return lines.join('\n')
1096
+ },
1097
+ })
1098
+
1025
1099
  registerTool({
1026
1100
  name: 'omni_memory',
1027
1101
  description: 'View or update Omni session memory. Actions: status, add (with type=project|decision|failure|trajectory and text), clear.',
@@ -1600,6 +1674,63 @@ export function apply(ctx, config = {}) {
1600
1674
  },
1601
1675
  })
1602
1676
 
1677
+ registerTool({
1678
+ name: 'omni_capability_performance',
1679
+ description: 'Track and inspect whether provisioned plugins/skills measurably improve task success, false completion, tokens, or tool errors.',
1680
+ parameters: {
1681
+ type: 'object',
1682
+ properties: {
1683
+ action: { type: 'string', enum: ['list', 'record', 'recommend'], description: 'Action (default list)' },
1684
+ provider: { type: 'string', description: 'Provider id' },
1685
+ successBefore: { type: 'number', description: 'Task success before provisioning' },
1686
+ successAfter: { type: 'number', description: 'Task success after provisioning' },
1687
+ falseCompletionBefore: { type: 'number', description: 'False completion rate before' },
1688
+ falseCompletionAfter: { type: 'number', description: 'False completion rate after' },
1689
+ tokensBefore: { type: 'number', description: 'Token usage before' },
1690
+ tokensAfter: { type: 'number', description: 'Token usage after' },
1691
+ toolErrorsBefore: { type: 'number', description: 'Tool errors before' },
1692
+ toolErrorsAfter: { type: 'number', description: 'Tool errors after' },
1693
+ usageDays: { type: 'number', description: 'Days since last use (for recommend)' },
1694
+ uniqueCapabilities: { type: 'number', description: 'Number of unique capabilities (for recommend)' },
1695
+ coveredBy: { type: 'array', items: { type: 'string' }, description: 'Other providers covering the same capabilities (for recommend)' },
1696
+ },
1697
+ required: [],
1698
+ },
1699
+ async execute(args) {
1700
+ const session = currentSession()
1701
+ const cwd = session?.meta?.cwd || session?.header?.cwd
1702
+ if (!cwd) return 'No workspace cwd found.'
1703
+ const action = args?.action || 'list'
1704
+ let registry = loadPerformanceRegistry(cwd)
1705
+ if (action === 'record') {
1706
+ if (!args?.provider) return 'provider is required for record.'
1707
+ registry = recordProvisionOutcome(registry, args.provider, {
1708
+ successBefore: args.successBefore,
1709
+ successAfter: args.successAfter,
1710
+ falseCompletionBefore: args.falseCompletionBefore,
1711
+ falseCompletionAfter: args.falseCompletionAfter,
1712
+ tokensBefore: args.tokensBefore,
1713
+ tokensAfter: args.tokensAfter,
1714
+ toolErrorsBefore: args.toolErrorsBefore,
1715
+ toolErrorsAfter: args.toolErrorsAfter,
1716
+ })
1717
+ const file = savePerformanceRegistry(cwd, registry)
1718
+ const value = evaluateProviderValue(registry.providers[args.provider])
1719
+ return `Recorded ${args.provider}: ${value.label} (${value.value})\nSaved: ${file}`
1720
+ }
1721
+ if (action === 'recommend') {
1722
+ if (!args?.provider) return 'provider is required for recommend.'
1723
+ const rec = recommendDemotion(registry, args.provider, {
1724
+ usageDays: Number(args.usageDays) || 0,
1725
+ uniqueCapabilities: Number(args.uniqueCapabilities) || 0,
1726
+ coveredBy: Array.isArray(args.coveredBy) ? args.coveredBy : [],
1727
+ })
1728
+ return `${rec.providerId}: ${rec.recommendation} — ${rec.reason}`
1729
+ }
1730
+ return formatPerformanceRegistry(registry)
1731
+ },
1732
+ })
1733
+
1603
1734
  registerTool({
1604
1735
  name: 'omni_mission_run',
1605
1736
  description: 'Run a Mission Planner loop with real subagents: Observe → Think → Act → Replan until completed or maxSteps.',
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Progressive Intelligence.
3
+ *
4
+ * Omni should be invisible for simple tasks and gradually unfold for hard
5
+ * ones. This module maps a TaskDecision to one of four run levels:
6
+ *
7
+ * L0 Direct — raw DSH + light verification
8
+ * L1 Assisted — context + skill + test
9
+ * L2 Orchestrated — plan + DAG + subagents + evidence
10
+ * L3 Guarded — deep plan + approval + independent verify + rollback
11
+ */
12
+
13
+ export const INTELLIGENCE_LEVELS = ['L0', 'L1', 'L2', 'L3']
14
+
15
+ export function decideIntelligenceLevel(decision = {}) {
16
+ const complexity = decision.complexity || 'direct'
17
+ const risk = decision.risk || 'low'
18
+ const taskType = decision.type || decision.taskType || 'other'
19
+
20
+ if (risk === 'high' || risk === 'critical') {
21
+ return {
22
+ level: 'L3',
23
+ label: 'Guarded',
24
+ useDag: true,
25
+ useSubagents: true,
26
+ approvalRequired: true,
27
+ verification: 'full',
28
+ independentVerify: true,
29
+ reasoningEffort: 'max',
30
+ contextMaxFiles: 20,
31
+ maxTaskAdditions: 1,
32
+ description: 'High-risk task: deep plan, approval, independent verify, rollback.',
33
+ }
34
+ }
35
+
36
+ if (complexity === 'plan' || (['feature', 'refactor'].includes(taskType) && complexity !== 'direct')) {
37
+ return {
38
+ level: 'L2',
39
+ label: 'Orchestrated',
40
+ useDag: true,
41
+ useSubagents: true,
42
+ approvalRequired: false,
43
+ verification: 'full',
44
+ independentVerify: false,
45
+ reasoningEffort: 'high',
46
+ contextMaxFiles: 12,
47
+ maxTaskAdditions: 1,
48
+ description: 'Multi-file or complex task: DAG + subagents + evidence.',
49
+ }
50
+ }
51
+
52
+ if (complexity === 'balanced' || ['bugfix', 'test'].includes(taskType)) {
53
+ return {
54
+ level: 'L1',
55
+ label: 'Assisted',
56
+ useDag: false,
57
+ useSubagents: false,
58
+ approvalRequired: false,
59
+ verification: 'standard',
60
+ independentVerify: false,
61
+ reasoningEffort: 'balanced',
62
+ contextMaxFiles: 6,
63
+ maxTaskAdditions: 0,
64
+ description: 'Normal task: focused context + skills + tests.',
65
+ }
66
+ }
67
+
68
+ return {
69
+ level: 'L0',
70
+ label: 'Direct',
71
+ useDag: false,
72
+ useSubagents: false,
73
+ approvalRequired: false,
74
+ verification: 'light',
75
+ independentVerify: false,
76
+ reasoningEffort: 'balanced',
77
+ contextMaxFiles: 3,
78
+ maxTaskAdditions: 0,
79
+ description: 'Simple task: raw DSH + light verification.',
80
+ }
81
+ }
82
+
83
+ export function formatIntelligenceLevel(level = {}) {
84
+ return `${level.level} ${level.label} — ${level.description}`
85
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Strategy Shift.
3
+ *
4
+ * Prevents blind retry loops: when the same failure category/reason repeats,
5
+ * Omni stops issuing another identical repair and instead re-investigates with
6
+ * broader context, a different capability, or a different role.
7
+ */
8
+
9
+ export function shouldShiftStrategy(failureHistory = []) {
10
+ if (failureHistory.length < 2) return false
11
+ const last = failureHistory[failureHistory.length - 1] || {}
12
+ const prev = failureHistory[failureHistory.length - 2] || {}
13
+ const sameCategory = last.category && last.category === prev.category
14
+ const sameReason = last.reason && last.reason === prev.reason
15
+ const sameHypothesis = last.hypothesis && last.hypothesis === prev.hypothesis
16
+ return Boolean(sameCategory && (sameReason || sameHypothesis))
17
+ }
18
+
19
+ export function nextStrategy(failureHistory = []) {
20
+ const last = failureHistory[failureHistory.length - 1] || {}
21
+ if (last.category === 'test_failure' || last.category === 'build_failure') {
22
+ return 'Re-investigate root cause with broader context and fresh capability set.'
23
+ }
24
+ if (last.category === 'permission_auth') {
25
+ return 'Check credentials/scopes and switch to an approved auth path.'
26
+ }
27
+ if (last.category === 'dependency_issue') {
28
+ return 'Re-resolve dependency graph and pin compatible versions.'
29
+ }
30
+ if (last.category === 'scope_change') {
31
+ return 'Re-plan with the user to narrow or clarify scope.'
32
+ }
33
+ return 'Expand context, re-diagnose from evidence, and try a different approach.'
34
+ }
@@ -163,3 +163,15 @@ test('runDagLoop calls onProgress after each batch', async () => {
163
163
  assert.ok(snapshots.length >= dag.tasks.length)
164
164
  assert.ok(snapshots.every((s) => s.dag && Array.isArray(s.actions)))
165
165
  })
166
+
167
+ test('runDagLoop triggers strategy shift after repeated same failure', async () => {
168
+ const dag = createMissionDag(buildMission('修复 bug', { taskType: 'bugfix' }))
169
+ const result = await runDagLoop(dag, {
170
+ act: async () => ({ ok: true }),
171
+ observe: async () => ({ type: 'test_failure', reason: 'same root cause' }),
172
+ maxSteps: 30,
173
+ maxParallel: 1,
174
+ })
175
+ const shiftTasks = result.dag.tasks.filter((t) => /Strategy shift/i.test(t.goal))
176
+ assert.ok(shiftTasks.length > 0)
177
+ })
@@ -1,12 +1,17 @@
1
1
  import test from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
+ import fs from 'node:fs'
4
+ import os from 'node:os'
5
+ import path from 'node:path'
3
6
 
4
7
  import {
5
8
  createPerformanceRegistry,
6
9
  evaluateProviderValue,
7
10
  formatPerformanceRegistry,
11
+ loadPerformanceRegistry,
8
12
  recommendDemotion,
9
13
  recordProvisionOutcome,
14
+ savePerformanceRegistry,
10
15
  } from '../src/capability-performance.mjs'
11
16
 
12
17
  test('recordProvisionOutcome stores before/after metrics', () => {
@@ -62,3 +67,18 @@ test('formatPerformanceRegistry renders entries', () => {
62
67
  registry = recordProvisionOutcome(registry, 'p', { successBefore: 0.5, successAfter: 0.9 })
63
68
  assert.match(formatPerformanceRegistry(registry), /p/)
64
69
  })
70
+
71
+ test('loadPerformanceRegistry and savePerformanceRegistry persist outcomes', () => {
72
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'cap-perf-'))
73
+ try {
74
+ let registry = createPerformanceRegistry()
75
+ registry = recordProvisionOutcome(registry, 'plugin-x', { successBefore: 0.7, successAfter: 0.9 })
76
+ const file = savePerformanceRegistry(cwd, registry)
77
+ assert.ok(fs.existsSync(file))
78
+ const loaded = loadPerformanceRegistry(cwd)
79
+ assert.ok(loaded.providers['plugin-x'])
80
+ assert.equal(evaluateProviderValue(loaded.providers['plugin-x']).label, 'high')
81
+ } finally {
82
+ fs.rmSync(cwd, { recursive: true, force: true })
83
+ }
84
+ })
@@ -0,0 +1,53 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ buildContextCapsule,
6
+ expandContextCapsule,
7
+ } from '../src/context-capsule.mjs'
8
+
9
+ const entries = [
10
+ { name: 'src/auth/session.ts', type: 'file' },
11
+ { name: 'src/middleware/auth.ts', type: 'file' },
12
+ { name: 'tests/session-concurrency.test.ts', type: 'file' },
13
+ { name: 'README.md', type: 'file' },
14
+ ]
15
+ const files = {
16
+ 'src/auth/session.ts': 'export function refreshSession() { /* refresh logic */ }',
17
+ 'src/middleware/auth.ts': 'import { refreshSession } from "../auth/session"',
18
+ 'tests/session-concurrency.test.ts': 'test("one refresh per session", () => {})',
19
+ 'README.md': '# Project readme',
20
+ }
21
+ const graph = {
22
+ 'src/middleware/auth.ts': [{ to: 'src/auth/session.ts', kind: 'import' }],
23
+ }
24
+
25
+ test('buildContextCapsule includes only relevant files and tests', () => {
26
+ const capsule = buildContextCapsule('Fix session refresh race condition', {
27
+ entries,
28
+ files,
29
+ graph,
30
+ needs: ['callers of refreshSession', 'tests related to concurrent refresh'],
31
+ invariants: ['one refresh request per session'],
32
+ failures: ['expected 1 refresh, got 3'],
33
+ maxFiles: 3,
34
+ })
35
+ assert.match(capsule, /src\/auth\/session\.ts/)
36
+ assert.match(capsule, /session-concurrency\.test\.ts/)
37
+ assert.match(capsule, /one refresh request per session/)
38
+ assert.match(capsule, /expected 1 refresh, got 3/)
39
+ assert.doesNotMatch(capsule, /README\.md/)
40
+ })
41
+
42
+ test('buildContextCapsule truncates to maxTotalChars', () => {
43
+ const capsule = buildContextCapsule('x', { entries, files, graph, maxTotalChars: 100 })
44
+ assert.ok(capsule.length <= 100 + 64)
45
+ assert.match(capsule, /truncated/)
46
+ })
47
+
48
+ test('expandContextCapsule appends requested context', () => {
49
+ const base = buildContextCapsule('fix login', { entries, files, graph })
50
+ const expanded = expandContextCapsule(base, ['tests related to concurrent refresh'], { entries, files, graph })
51
+ assert.match(expanded, /additional context requested/)
52
+ assert.match(expanded, /session-concurrency/)
53
+ })
@@ -0,0 +1,39 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ INTELLIGENCE_LEVELS,
6
+ decideIntelligenceLevel,
7
+ formatIntelligenceLevel,
8
+ } from '../src/progressive-intelligence.mjs'
9
+
10
+ test('decideIntelligenceLevel maps direct low-risk tasks to L0', () => {
11
+ const level = decideIntelligenceLevel({ complexity: 'direct', risk: 'low', type: 'other' })
12
+ assert.equal(level.level, 'L0')
13
+ assert.equal(level.useDag, false)
14
+ assert.equal(level.verification, 'light')
15
+ })
16
+
17
+ test('decideIntelligenceLevel maps balanced coding tasks to L1', () => {
18
+ const level = decideIntelligenceLevel({ complexity: 'balanced', risk: 'low', type: 'bugfix' })
19
+ assert.equal(level.level, 'L1')
20
+ assert.equal(level.useSubagents, false)
21
+ })
22
+
23
+ test('decideIntelligenceLevel maps plan complexity to L2', () => {
24
+ const level = decideIntelligenceLevel({ complexity: 'plan', risk: 'low', type: 'feature' })
25
+ assert.equal(level.level, 'L2')
26
+ assert.equal(level.useDag, true)
27
+ })
28
+
29
+ test('decideIntelligenceLevel maps high risk to L3 guarded', () => {
30
+ const level = decideIntelligenceLevel({ complexity: 'direct', risk: 'high', type: 'bugfix' })
31
+ assert.equal(level.level, 'L3')
32
+ assert.equal(level.approvalRequired, true)
33
+ assert.equal(level.independentVerify, true)
34
+ })
35
+
36
+ test('INTELLIGENCE_LEVELS and formatter are stable', () => {
37
+ assert.deepEqual(INTELLIGENCE_LEVELS, ['L0', 'L1', 'L2', 'L3'])
38
+ assert.match(formatIntelligenceLevel({ level: 'L2', label: 'Orchestrated', description: 'x' }), /L2 Orchestrated/)
39
+ })
@@ -0,0 +1,28 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+
4
+ import {
5
+ nextStrategy,
6
+ shouldShiftStrategy,
7
+ } from '../src/strategy-shift.mjs'
8
+
9
+ test('shouldShiftStrategy returns false for single or different failures', () => {
10
+ assert.equal(shouldShiftStrategy([]), false)
11
+ assert.equal(shouldShiftStrategy([{ category: 'test_failure', reason: 'a' }]), false)
12
+ assert.equal(shouldShiftStrategy([
13
+ { category: 'test_failure', reason: 'a' },
14
+ { category: 'build_failure', reason: 'b' },
15
+ ]), false)
16
+ })
17
+
18
+ test('shouldShiftStrategy returns true when same category and reason repeat', () => {
19
+ assert.equal(shouldShiftStrategy([
20
+ { category: 'test_failure', reason: 'same', hypothesis: 'h' },
21
+ { category: 'test_failure', reason: 'same', hypothesis: 'h' },
22
+ ]), true)
23
+ })
24
+
25
+ test('nextStrategy returns a recovery suggestion', () => {
26
+ const strategy = nextStrategy([{ category: 'test_failure' }])
27
+ assert.match(strategy, /Re-investigate/)
28
+ })