ghost-dragon 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/.github/workflows/ci.yml +23 -0
  2. package/CHANGELOG.md +96 -0
  3. package/README.md +193 -0
  4. package/bootstrap.ps1 +83 -0
  5. package/bootstrap.sh +71 -0
  6. package/dist/agent/loop.d.ts +68 -0
  7. package/dist/agent/loop.d.ts.map +1 -0
  8. package/dist/agent/loop.js +135 -0
  9. package/dist/agent/mcp.d.ts +33 -0
  10. package/dist/agent/mcp.d.ts.map +1 -0
  11. package/dist/agent/mcp.js +107 -0
  12. package/dist/agent/session.d.ts +16 -0
  13. package/dist/agent/session.d.ts.map +1 -0
  14. package/dist/agent/session.js +55 -0
  15. package/dist/agent/skills.d.ts +36 -0
  16. package/dist/agent/skills.d.ts.map +1 -0
  17. package/dist/agent/skills.js +153 -0
  18. package/dist/agent/stack.d.ts +21 -0
  19. package/dist/agent/stack.d.ts.map +1 -0
  20. package/dist/agent/stack.js +158 -0
  21. package/dist/agent/task.d.ts +21 -0
  22. package/dist/agent/task.d.ts.map +1 -0
  23. package/dist/agent/task.js +45 -0
  24. package/dist/agent/tools.d.ts +44 -0
  25. package/dist/agent/tools.d.ts.map +1 -0
  26. package/dist/agent/tools.js +262 -0
  27. package/dist/agent/trace.d.ts +34 -0
  28. package/dist/agent/trace.d.ts.map +1 -0
  29. package/dist/agent/trace.js +72 -0
  30. package/dist/agent.d.ts +46 -0
  31. package/dist/agent.d.ts.map +1 -0
  32. package/dist/agent.js +103 -0
  33. package/dist/auth.d.ts +74 -0
  34. package/dist/auth.d.ts.map +1 -0
  35. package/dist/auth.js +116 -0
  36. package/dist/brain/anthropic.d.ts +19 -0
  37. package/dist/brain/anthropic.d.ts.map +1 -0
  38. package/dist/brain/anthropic.js +74 -0
  39. package/dist/brain/claude-cli.d.ts +20 -0
  40. package/dist/brain/claude-cli.d.ts.map +1 -0
  41. package/dist/brain/claude-cli.js +79 -0
  42. package/dist/brain/ghost-ember.d.ts +28 -0
  43. package/dist/brain/ghost-ember.d.ts.map +1 -0
  44. package/dist/brain/ghost-ember.js +97 -0
  45. package/dist/brain/index.d.ts +22 -0
  46. package/dist/brain/index.d.ts.map +1 -0
  47. package/dist/brain/index.js +95 -0
  48. package/dist/brain/openai-compat.d.ts +21 -0
  49. package/dist/brain/openai-compat.d.ts.map +1 -0
  50. package/dist/brain/openai-compat.js +119 -0
  51. package/dist/brain/router/classify.d.ts +23 -0
  52. package/dist/brain/router/classify.d.ts.map +1 -0
  53. package/dist/brain/router/classify.js +160 -0
  54. package/dist/brain/router/execute.d.ts +23 -0
  55. package/dist/brain/router/execute.d.ts.map +1 -0
  56. package/dist/brain/router/execute.js +84 -0
  57. package/dist/brain/router/index.d.ts +26 -0
  58. package/dist/brain/router/index.d.ts.map +1 -0
  59. package/dist/brain/router/index.js +118 -0
  60. package/dist/brain/router/routing-memory.d.ts +27 -0
  61. package/dist/brain/router/routing-memory.d.ts.map +1 -0
  62. package/dist/brain/router/routing-memory.js +77 -0
  63. package/dist/brain/router/select.d.ts +32 -0
  64. package/dist/brain/router/select.d.ts.map +1 -0
  65. package/dist/brain/router/select.js +146 -0
  66. package/dist/brain/router/two-hop.d.ts +23 -0
  67. package/dist/brain/router/two-hop.d.ts.map +1 -0
  68. package/dist/brain/router/two-hop.js +39 -0
  69. package/dist/brain/router/verify.d.ts +37 -0
  70. package/dist/brain/router/verify.d.ts.map +1 -0
  71. package/dist/brain/router/verify.js +111 -0
  72. package/dist/brain/types.d.ts +55 -0
  73. package/dist/brain/types.d.ts.map +1 -0
  74. package/dist/brain/types.js +16 -0
  75. package/dist/brain/worker.d.ts +27 -0
  76. package/dist/brain/worker.d.ts.map +1 -0
  77. package/dist/brain/worker.js +71 -0
  78. package/dist/commands/ai.d.ts +24 -0
  79. package/dist/commands/ai.d.ts.map +1 -0
  80. package/dist/commands/ai.js +137 -0
  81. package/dist/commands/alerts.d.ts +19 -0
  82. package/dist/commands/alerts.d.ts.map +1 -0
  83. package/dist/commands/alerts.js +114 -0
  84. package/dist/commands/billing.d.ts +13 -0
  85. package/dist/commands/billing.d.ts.map +1 -0
  86. package/dist/commands/billing.js +55 -0
  87. package/dist/commands/chat.d.ts +22 -0
  88. package/dist/commands/chat.d.ts.map +1 -0
  89. package/dist/commands/chat.js +422 -0
  90. package/dist/commands/config.d.ts +18 -0
  91. package/dist/commands/config.d.ts.map +1 -0
  92. package/dist/commands/config.js +136 -0
  93. package/dist/commands/doctor.d.ts +11 -0
  94. package/dist/commands/doctor.d.ts.map +1 -0
  95. package/dist/commands/doctor.js +73 -0
  96. package/dist/commands/global.d.ts +11 -0
  97. package/dist/commands/global.d.ts.map +1 -0
  98. package/dist/commands/global.js +253 -0
  99. package/dist/commands/keep.d.ts +12 -0
  100. package/dist/commands/keep.d.ts.map +1 -0
  101. package/dist/commands/keep.js +58 -0
  102. package/dist/commands/lifecycle.d.ts +17 -0
  103. package/dist/commands/lifecycle.d.ts.map +1 -0
  104. package/dist/commands/lifecycle.js +267 -0
  105. package/dist/commands/login.d.ts +16 -0
  106. package/dist/commands/login.d.ts.map +1 -0
  107. package/dist/commands/login.js +234 -0
  108. package/dist/commands/maintenance.d.ts +12 -0
  109. package/dist/commands/maintenance.d.ts.map +1 -0
  110. package/dist/commands/maintenance.js +76 -0
  111. package/dist/commands/mcp.d.ts +16 -0
  112. package/dist/commands/mcp.d.ts.map +1 -0
  113. package/dist/commands/mcp.js +56 -0
  114. package/dist/commands/memory.d.ts +13 -0
  115. package/dist/commands/memory.d.ts.map +1 -0
  116. package/dist/commands/memory.js +218 -0
  117. package/dist/commands/osint.d.ts +14 -0
  118. package/dist/commands/osint.d.ts.map +1 -0
  119. package/dist/commands/osint.js +161 -0
  120. package/dist/commands/pentest.d.ts +13 -0
  121. package/dist/commands/pentest.d.ts.map +1 -0
  122. package/dist/commands/pentest.js +131 -0
  123. package/dist/commands/scale.d.ts +14 -0
  124. package/dist/commands/scale.d.ts.map +1 -0
  125. package/dist/commands/scale.js +191 -0
  126. package/dist/commands/serve.d.ts +16 -0
  127. package/dist/commands/serve.d.ts.map +1 -0
  128. package/dist/commands/serve.js +167 -0
  129. package/dist/commands/tui.d.ts +17 -0
  130. package/dist/commands/tui.d.ts.map +1 -0
  131. package/dist/commands/tui.js +138 -0
  132. package/dist/commands/wyrm.d.ts +20 -0
  133. package/dist/commands/wyrm.d.ts.map +1 -0
  134. package/dist/commands/wyrm.js +274 -0
  135. package/dist/config.d.ts +67 -0
  136. package/dist/config.d.ts.map +1 -0
  137. package/dist/config.js +54 -0
  138. package/dist/index.d.ts +16 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +85 -0
  141. package/dist/manifest.d.ts +31 -0
  142. package/dist/manifest.d.ts.map +1 -0
  143. package/dist/manifest.js +83 -0
  144. package/dist/ui.d.ts +57 -0
  145. package/dist/ui.d.ts.map +1 -0
  146. package/dist/ui.js +174 -0
  147. package/dist/utils.d.ts +33 -0
  148. package/dist/utils.d.ts.map +1 -0
  149. package/dist/utils.js +155 -0
  150. package/dist/wyrm/mcp.d.ts +37 -0
  151. package/dist/wyrm/mcp.d.ts.map +1 -0
  152. package/dist/wyrm/mcp.js +137 -0
  153. package/docs/SYSTEM-PREMORTEM.md +397 -0
  154. package/dragon-manifest.toml +241 -0
  155. package/dragon.py +177 -0
  156. package/install/launchd/lk.ghosts.dragonkeep.plist +57 -0
  157. package/install/systemd/dragonkeep.service +40 -0
  158. package/media/dragon-silver-lockup.svg +931 -0
  159. package/media/dragon-silver-mark.svg +931 -0
  160. package/media/dragon-silver.png +0 -0
  161. package/package.json +45 -0
  162. package/specs/001-godmode/constitution.md +54 -0
  163. package/specs/001-godmode/plan.md +30 -0
  164. package/specs/001-godmode/spec.md +64 -0
  165. package/specs/001-godmode/tasks.md +35 -0
  166. package/specs/002-premortem-positioning/premortem.md +211 -0
  167. package/src/agent/loop.ts +165 -0
  168. package/src/agent/mcp.ts +92 -0
  169. package/src/agent/session.ts +48 -0
  170. package/src/agent/skills.ts +138 -0
  171. package/src/agent/stack.ts +154 -0
  172. package/src/agent/task.ts +55 -0
  173. package/src/agent/tools.ts +255 -0
  174. package/src/agent/trace.ts +76 -0
  175. package/src/agent.ts +114 -0
  176. package/src/auth.ts +133 -0
  177. package/src/brain/anthropic.ts +83 -0
  178. package/src/brain/claude-cli.ts +78 -0
  179. package/src/brain/ghost-ember.ts +94 -0
  180. package/src/brain/index.ts +99 -0
  181. package/src/brain/openai-compat.ts +115 -0
  182. package/src/brain/router/classify.ts +167 -0
  183. package/src/brain/router/execute.ts +80 -0
  184. package/src/brain/router/index.ts +125 -0
  185. package/src/brain/router/routing-memory.ts +71 -0
  186. package/src/brain/router/select.ts +156 -0
  187. package/src/brain/router/two-hop.ts +62 -0
  188. package/src/brain/router/verify.ts +123 -0
  189. package/src/brain/types.ts +61 -0
  190. package/src/brain/worker.ts +72 -0
  191. package/src/commands/ai.ts +144 -0
  192. package/src/commands/alerts.ts +131 -0
  193. package/src/commands/billing.ts +59 -0
  194. package/src/commands/chat.ts +318 -0
  195. package/src/commands/config.ts +137 -0
  196. package/src/commands/doctor.ts +71 -0
  197. package/src/commands/global.ts +256 -0
  198. package/src/commands/keep.ts +67 -0
  199. package/src/commands/lifecycle.ts +273 -0
  200. package/src/commands/login.ts +184 -0
  201. package/src/commands/maintenance.ts +54 -0
  202. package/src/commands/mcp.ts +57 -0
  203. package/src/commands/memory.ts +229 -0
  204. package/src/commands/osint.ts +171 -0
  205. package/src/commands/pentest.ts +140 -0
  206. package/src/commands/scale.ts +185 -0
  207. package/src/commands/serve.ts +171 -0
  208. package/src/commands/tui.ts +126 -0
  209. package/src/commands/wyrm.ts +269 -0
  210. package/src/config.ts +93 -0
  211. package/src/index.ts +92 -0
  212. package/src/manifest.ts +104 -0
  213. package/src/ui.ts +188 -0
  214. package/src/utils.ts +153 -0
  215. package/src/wyrm/mcp.ts +130 -0
  216. package/test/auth.test.ts +70 -0
  217. package/test/brain.test.ts +39 -0
  218. package/test/security.test.ts +104 -0
  219. package/test/skills.test.ts +38 -0
  220. package/test/ui.test.ts +46 -0
  221. package/tsconfig.json +19 -0
  222. package/worker/package-lock.json +1527 -0
  223. package/worker/package.json +17 -0
  224. package/worker/src/index.ts +76 -0
  225. package/worker/tsconfig.json +15 -0
  226. package/worker/wrangler.toml +26 -0
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Ghost Router — the spine of the multi-model stack (ROUTER-BLUEPRINT.md §1).
3
+ *
4
+ * It IS a Brain, so the agent loop stays unchanged: every turn it classifies the
5
+ * request (intent × difficulty × stakes), selects the best {provider, model} for
6
+ * 8 GB, then DELEGATES to that underlying brain. The factory is injected as
7
+ * `resolve` to avoid a circular import with brain/index.ts.
8
+ *
9
+ * Each decision is appended to ~/.dragon/routing.jsonl (observability + the
10
+ * DragonSpark flywheel) and shown to the operator on stderr (silence with
11
+ * DRAGON_ROUTER_QUIET=1).
12
+ *
13
+ * MVP scope: single-hop selection over the resident Ollama models + Claude
14
+ * escalation. The reason→tool two-hop and llama-swap co-residency are later phases.
15
+ *
16
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
17
+ */
18
+ import type { Brain } from '../types.js';
19
+ export interface RouterOpts {
20
+ /** Factory injected by brain/index.ts to avoid a circular import. */
21
+ resolve: (provider: string, model?: string) => Brain;
22
+ /** Ollama base (…/v1) used for embeddings + tags/ps lookups. */
23
+ localBaseURL: string;
24
+ }
25
+ export declare function makeRouterBrain(opts: RouterOpts): Brain;
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/brain/router/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAKH,OAAO,KAAK,EAAE,KAAK,EAAuB,MAAM,aAAa,CAAA;AAoB7D,MAAM,WAAW,UAAU;IACzB,qEAAqE;IACrE,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,KAAK,CAAA;IACpD,gEAAgE;IAChE,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,KAAK,CA4EvD"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Ghost Router — the spine of the multi-model stack (ROUTER-BLUEPRINT.md §1).
3
+ *
4
+ * It IS a Brain, so the agent loop stays unchanged: every turn it classifies the
5
+ * request (intent × difficulty × stakes), selects the best {provider, model} for
6
+ * 8 GB, then DELEGATES to that underlying brain. The factory is injected as
7
+ * `resolve` to avoid a circular import with brain/index.ts.
8
+ *
9
+ * Each decision is appended to ~/.dragon/routing.jsonl (observability + the
10
+ * DragonSpark flywheel) and shown to the operator on stderr (silence with
11
+ * DRAGON_ROUTER_QUIET=1).
12
+ *
13
+ * MVP scope: single-hop selection over the resident Ollama models + Claude
14
+ * escalation. The reason→tool two-hop and llama-swap co-residency are later phases.
15
+ *
16
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
17
+ */
18
+ import { appendFileSync, mkdirSync } from 'node:fs';
19
+ import { homedir } from 'node:os';
20
+ import { join } from 'node:path';
21
+ import { classify } from './classify.js';
22
+ import { selectTarget } from './select.js';
23
+ import { verifyReasoning } from './verify.js';
24
+ import { twoHop } from './two-hop.js';
25
+ const ROUTE_LOG = process.env.DRAGON_ROUTING_LOG || join(homedir(), '.dragon', 'routing.jsonl');
26
+ function logDecision(rec) {
27
+ try {
28
+ mkdirSync(join(homedir(), '.dragon'), { recursive: true });
29
+ appendFileSync(ROUTE_LOG, JSON.stringify({ ts: new Date().toISOString(), ...rec }) + '\n');
30
+ }
31
+ catch { /* best-effort */ }
32
+ }
33
+ function show(line) {
34
+ if (process.env.DRAGON_ROUTER_QUIET === '1')
35
+ return;
36
+ try {
37
+ process.stderr.write(`\x1b[2m⟐ ${line}\x1b[0m\n`);
38
+ }
39
+ catch { /* ignore */ }
40
+ }
41
+ export function makeRouterBrain(opts) {
42
+ const cache = new Map();
43
+ const get = (provider, model) => {
44
+ const safe = provider === 'router' ? 'local' : provider; // never recurse
45
+ const key = `${safe}:${model ?? ''}`;
46
+ let b = cache.get(key);
47
+ if (!b) {
48
+ b = opts.resolve(safe, model);
49
+ cache.set(key, b);
50
+ }
51
+ return b;
52
+ };
53
+ return {
54
+ id: 'router',
55
+ model: 'auto',
56
+ async turn(t) {
57
+ const hasTools = t.tools.length > 0;
58
+ const c = await classify(opts.localBaseURL, t.messages, t.tools.length, t.signal);
59
+ const target = await selectTarget(c, hasTools, opts.localBaseURL, t.signal);
60
+ const label = `${target.provider}${target.model ? ':' + target.model : ''}`;
61
+ const base = {
62
+ intent: c.intent, difficulty: c.difficulty, stakes: c.stakes, via: c.via,
63
+ hasTools, provider: target.provider, model: target.model ?? null,
64
+ swap: target.swap, penalty: target.penalty ?? 0, why: target.why,
65
+ };
66
+ // Reason→tool two-hop: the reasoner plans (no tools), the workhorse executes.
67
+ if (target.twoHop && target.provider === 'local' && target.model && target.reasoner) {
68
+ show(`router → two-hop: ${target.reasoner} plans → ${target.model} executes [${c.intent}/${c.difficulty.toFixed(2)}/${c.stakes}]`);
69
+ try {
70
+ const { turn, planChars } = await twoHop(opts.localBaseURL, target.reasoner, target.model, t, get);
71
+ show(` ↳ plan ${planChars} chars → ${turn.toolCalls.length} tool call(s)`);
72
+ logDecision({ ...base, twoHop: true, planChars, toolCalls: turn.toolCalls.length });
73
+ return turn;
74
+ }
75
+ catch (e) {
76
+ show(` ↳ two-hop failed (${e.message}) — single call`);
77
+ // Log the failure so routing-memory accrues a penalty (a config where
78
+ // two-hop keeps failing — e.g. swap OOM/timeout — adaptively backs off).
79
+ logDecision({ ...base, twoHop: true, twoHopFailed: true });
80
+ }
81
+ }
82
+ // Verified hard-reasoning path: best-of-N + vote (+ optional execution check),
83
+ // then a CONFIDENCE CASCADE — escalate to Claude on low agreement / failed
84
+ // execution. A model with a bad routing-memory track record escalates sooner.
85
+ if (target.verify && target.provider === 'local' && target.model) {
86
+ const votes = Math.max(1, parseInt(process.env.DRAGON_ROUTER_VOTES || '3', 10) || 3);
87
+ show(`router → ${label} ×${votes} verified [${c.intent}/${c.difficulty.toFixed(2)}/${c.stakes}]`);
88
+ try {
89
+ const { turn, meta } = await verifyReasoning(opts.localBaseURL, target.model, t, votes);
90
+ const execFail = !!(meta.exec?.ran && meta.exec.ok === false);
91
+ show(` ↳ agreement ${meta.agreement ?? 'n/a'} (${meta.votes} votes${meta.exec?.ran ? `, exec ${meta.exec.ok ? 'pass' : 'fail'}` : ''})`);
92
+ const floor = parseFloat(process.env.DRAGON_ROUTER_ESCALATE_BELOW || '0.5') || 0.5;
93
+ const threshold = Math.min(0.85, floor + (target.penalty ?? 0) * 0.3); // worse history → escalate sooner
94
+ const lowConf = meta.agreement != null && meta.agreement < threshold;
95
+ if ((lowConf || execFail) && process.env.DRAGON_ROUTER_NO_ESCALATE !== '1') {
96
+ try {
97
+ const claude = get('claude');
98
+ show(` ↳ ${execFail ? 'execution failed' : `low confidence (${meta.agreement} < ${threshold.toFixed(2)})`} → escalating to Claude`);
99
+ const esc = await claude.turn(t);
100
+ logDecision({ ...base, verify: meta, escalated: true, reason: execFail ? 'exec-fail' : 'low-agreement', threshold });
101
+ return esc;
102
+ }
103
+ catch { /* Claude unavailable → keep the verified local answer */ }
104
+ }
105
+ logDecision({ ...base, verify: meta, escalated: false });
106
+ return turn;
107
+ }
108
+ catch (e) {
109
+ show(` ↳ verify failed (${e.message}) — single call`);
110
+ }
111
+ }
112
+ show(`router → ${label} [${c.intent}/${c.difficulty.toFixed(2)}/${c.stakes}${target.swap ? ' · swap' : ''}] ${target.why}`);
113
+ logDecision(base);
114
+ return get(target.provider, target.model).turn(t);
115
+ },
116
+ };
117
+ }
118
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Routing-memory — negative learning over the router's own history
3
+ * (ROUTER-BLUEPRINT.md §3, dragon-cli-local MVP).
4
+ *
5
+ * The router already logs every decision (+ verify agreement + exec pass/fail +
6
+ * whether it escalated) to ~/.dragon/routing.jsonl. This reads that log and, per
7
+ * (intent, model), computes a PENALTY in [0..1]: how often that model needed help
8
+ * for that kind of task (low agreement, failed execution, or had to escalate). The
9
+ * selector uses it to demote chronically-failing models, and the cascade uses it to
10
+ * escalate sooner for a model with a bad track record.
11
+ *
12
+ * Local now; promotable to the Wyrm memory substrate (the wyrm-routing-rerank
13
+ * subsystem) — same signal, durable + cross-device.
14
+ *
15
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
16
+ */
17
+ interface Stat {
18
+ n: number;
19
+ bad: number;
20
+ }
21
+ type Stats = Record<string, Record<string, Stat>>;
22
+ /** 0 = no opinion / reliable; →1 = this model keeps needing help for this intent. */
23
+ export declare function penalty(intent: string, model: string): number;
24
+ /** For telemetry / `dragon` introspection. */
25
+ export declare function routingStats(): Stats;
26
+ export {};
27
+ //# sourceMappingURL=routing-memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routing-memory.d.ts","sourceRoot":"","sources":["../../../src/brain/router/routing-memory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAUH,UAAU,IAAI;IAAG,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AACzC,KAAK,KAAK,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;AAkCjD,qFAAqF;AACrF,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED,8CAA8C;AAC9C,wBAAgB,YAAY,IAAI,KAAK,CAEpC"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Routing-memory — negative learning over the router's own history
3
+ * (ROUTER-BLUEPRINT.md §3, dragon-cli-local MVP).
4
+ *
5
+ * The router already logs every decision (+ verify agreement + exec pass/fail +
6
+ * whether it escalated) to ~/.dragon/routing.jsonl. This reads that log and, per
7
+ * (intent, model), computes a PENALTY in [0..1]: how often that model needed help
8
+ * for that kind of task (low agreement, failed execution, or had to escalate). The
9
+ * selector uses it to demote chronically-failing models, and the cascade uses it to
10
+ * escalate sooner for a model with a bad track record.
11
+ *
12
+ * Local now; promotable to the Wyrm memory substrate (the wyrm-routing-rerank
13
+ * subsystem) — same signal, durable + cross-device.
14
+ *
15
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
16
+ */
17
+ import { readFileSync } from 'node:fs';
18
+ import { homedir } from 'node:os';
19
+ import { join } from 'node:path';
20
+ const LOG = process.env.DRAGON_ROUTING_LOG || join(homedir(), '.dragon', 'routing.jsonl');
21
+ const MIN_SAMPLES = 3; // below this we have no opinion (penalty 0)
22
+ const LOW_AGREEMENT = 0.5;
23
+ let cache = null;
24
+ function compute() {
25
+ const stats = {};
26
+ let raw;
27
+ try {
28
+ raw = readFileSync(LOG, 'utf-8');
29
+ }
30
+ catch {
31
+ return stats;
32
+ }
33
+ for (const line of raw.split('\n')) {
34
+ if (!line.trim())
35
+ continue;
36
+ let r;
37
+ try {
38
+ r = JSON.parse(line);
39
+ }
40
+ catch {
41
+ continue;
42
+ }
43
+ const intent = r.intent, model = r.model;
44
+ if (!intent || !model)
45
+ continue;
46
+ const v = r.verify || {};
47
+ const lowAgree = typeof v.agreement === 'number' && v.agreement < LOW_AGREEMENT;
48
+ const execFail = v.exec && v.exec.ran && v.exec.ok === false;
49
+ const escalated = r.escalated === true;
50
+ const twoHopFailed = r.twoHopFailed === true;
51
+ const bad = lowAgree || execFail || escalated || twoHopFailed;
52
+ const byModel = (stats[intent] ||= {});
53
+ const s = (byModel[model] ||= { n: 0, bad: 0 });
54
+ s.n++;
55
+ if (bad)
56
+ s.bad++;
57
+ }
58
+ return stats;
59
+ }
60
+ function load() {
61
+ if (cache && Date.now() - cache.at < 30_000)
62
+ return cache.stats;
63
+ cache = { at: Date.now(), stats: compute() };
64
+ return cache.stats;
65
+ }
66
+ /** 0 = no opinion / reliable; →1 = this model keeps needing help for this intent. */
67
+ export function penalty(intent, model) {
68
+ const s = load()[intent]?.[model];
69
+ if (!s || s.n < MIN_SAMPLES)
70
+ return 0;
71
+ return Math.max(0, Math.min(1, s.bad / s.n));
72
+ }
73
+ /** For telemetry / `dragon` introspection. */
74
+ export function routingStats() {
75
+ return load();
76
+ }
77
+ //# sourceMappingURL=routing-memory.js.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Ghost Router selection policy — (intent × difficulty × stakes × hasTools) → a
3
+ * concrete {provider, model}, constrained by what's actually installed and what
4
+ * fits 8 GB.
5
+ *
6
+ * Roles (Ollama tags, all overridable via env/config):
7
+ * workhorse (tool/agent turns) → mistral-nemo (verified tool-caller)
8
+ * reasoner (hard, NO tools) → vibethinker (can't tool-call → only when tools=[])
9
+ * cheap (simple chat) → qwen2.5:1.5b (fast, tiny)
10
+ * escalate (high stakes / hard) → claude (cloud, only if available)
11
+ *
12
+ * VRAM rule: only one big model fits at a time. When two candidates are equally
13
+ * acceptable, prefer the one already resident in Ollama (avoids a reload/“swap”).
14
+ * EMBER is intentionally NOT a default role yet — it earns its way in via the
15
+ * DragonSpark flywheel.
16
+ *
17
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
18
+ */
19
+ import type { Classification } from './classify.js';
20
+ export interface Target {
21
+ provider: string;
22
+ model?: string;
23
+ why: string;
24
+ swap: boolean;
25
+ verify?: boolean;
26
+ penalty?: number;
27
+ twoHop?: boolean;
28
+ reasoner?: string;
29
+ }
30
+ /** Decide where this turn goes. Pure policy + availability; never throws. */
31
+ export declare function selectTarget(c: Classification, hasTools: boolean, base: string, signal?: AbortSignal): Promise<Target>;
32
+ //# sourceMappingURL=select.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../../src/brain/router/select.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAGnD,MAAM,WAAW,MAAM;IACrB,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,OAAO,CAAA;IACb,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAgDD,6EAA6E;AAC7E,wBAAsB,YAAY,CAChC,CAAC,EAAE,cAAc,EACjB,QAAQ,EAAE,OAAO,EACjB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,CAAC,CAoEjB"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Ghost Router selection policy — (intent × difficulty × stakes × hasTools) → a
3
+ * concrete {provider, model}, constrained by what's actually installed and what
4
+ * fits 8 GB.
5
+ *
6
+ * Roles (Ollama tags, all overridable via env/config):
7
+ * workhorse (tool/agent turns) → mistral-nemo (verified tool-caller)
8
+ * reasoner (hard, NO tools) → vibethinker (can't tool-call → only when tools=[])
9
+ * cheap (simple chat) → qwen2.5:1.5b (fast, tiny)
10
+ * escalate (high stakes / hard) → claude (cloud, only if available)
11
+ *
12
+ * VRAM rule: only one big model fits at a time. When two candidates are equally
13
+ * acceptable, prefer the one already resident in Ollama (avoids a reload/“swap”).
14
+ * EMBER is intentionally NOT a default role yet — it earns its way in via the
15
+ * DragonSpark flywheel.
16
+ *
17
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
18
+ */
19
+ import { execSync } from 'node:child_process';
20
+ import { loadConfig } from '../../config.js';
21
+ import { penalty } from './routing-memory.js';
22
+ const env = (k, d) => process.env[k] || d;
23
+ function roles() {
24
+ const c = loadConfig().brain;
25
+ return {
26
+ workhorse: env('DRAGON_ROUTER_WORKHORSE', c?.routerWorkhorse || 'mistral-nemo'),
27
+ reasoner: env('DRAGON_ROUTER_REASONER', c?.routerReasoner || 'vibethinker'),
28
+ cheap: env('DRAGON_ROUTER_CHEAP', c?.routerCheap || 'qwen2.5:1.5b'),
29
+ };
30
+ }
31
+ const OLLAMA = (base) => base.replace(/\/v1\/?$/, '').replace(/\/+$/, '');
32
+ let installedCache = null;
33
+ async function installed(base, signal) {
34
+ if (installedCache && Date.now() - installedCache.at < 60_000)
35
+ return installedCache.names;
36
+ try {
37
+ const res = await fetch(OLLAMA(base) + '/api/tags', { signal });
38
+ if (!res.ok)
39
+ return new Set(); // don't cache an error as "nothing installed"
40
+ const data = (await res.json());
41
+ const names = new Set((data.models || []).map((m) => m.name.replace(/:latest$/, '')));
42
+ installedCache = { at: Date.now(), names };
43
+ return names;
44
+ }
45
+ catch {
46
+ return new Set();
47
+ }
48
+ }
49
+ async function resident(base, signal) {
50
+ try {
51
+ const res = await fetch(OLLAMA(base) + '/api/ps', { signal });
52
+ if (!res.ok)
53
+ return new Set();
54
+ const data = (await res.json());
55
+ return new Set((data.models || []).map((m) => m.name.replace(/:latest$/, '')));
56
+ }
57
+ catch {
58
+ return new Set();
59
+ }
60
+ }
61
+ function claudeAvailable() {
62
+ const cfg = loadConfig();
63
+ if (process.env.ANTHROPIC_API_KEY || cfg.brain?.keys?.anthropic)
64
+ return true;
65
+ try {
66
+ execSync('command -v claude', { stdio: 'ignore' });
67
+ return true;
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
73
+ const has = (set, name) => set.has(name) || set.has(name.replace(/:latest$/, ''));
74
+ /** Decide where this turn goes. Pure policy + availability; never throws. */
75
+ export async function selectTarget(c, hasTools, base, signal) {
76
+ const r = roles();
77
+ const have = await installed(base, signal);
78
+ const live = await resident(base, signal);
79
+ const local = (model, why) => ({
80
+ provider: 'local', model, why, swap: !has(live, model),
81
+ });
82
+ // 1) Stakes → escalate to Claude. PRIVACY-FIRST: this ships the turn to the cloud,
83
+ // so it is OPT-IN (DRAGON_ROUTER_ESCALATE_STAKES=1) and NEVER fires for
84
+ // security-sensitive content. `security` stakes (credentials, ssh, secrets,
85
+ // prod, sudo) and any ops_security intent ALWAYS stay local — we never ship
86
+ // secrets or target data off the box. Only financial/critical business-
87
+ // irreversible decisions may escalate, and only when explicitly enabled.
88
+ // (Low-confidence reasoning still escalates separately via the verify cascade.)
89
+ if (process.env.DRAGON_ROUTER_ESCALATE_STAKES === '1' &&
90
+ (c.stakes === 'financial' || c.stakes === 'critical') &&
91
+ c.intent !== 'ops_security' &&
92
+ claudeAvailable()) {
93
+ return { provider: 'claude', why: `stakes=${c.stakes} → escalate to Claude (opt-in)`, swap: false };
94
+ }
95
+ // 2) Reasoning with NO tools in play → the reasoning specialist (that's exactly
96
+ // what it's for; it can't tool-call, so only ever route here when tools=[]).
97
+ // Opt into the verified (best-of-N) path with DRAGON_ROUTER_VERIFY=1.
98
+ if (!hasTools && c.intent === 'reasoning' && has(have, r.reasoner)) {
99
+ const t = local(r.reasoner, `reasoning, no tools → ${r.reasoner}`);
100
+ t.verify = process.env.DRAGON_ROUTER_VERIFY === '1';
101
+ t.penalty = penalty(c.intent, r.reasoner);
102
+ return t;
103
+ }
104
+ // 3) Simple chat, no tools → the cheap/tiny model.
105
+ if (!hasTools && c.intent === 'chat' && c.difficulty < 0.35 && has(have, r.cheap)) {
106
+ return local(r.cheap, `simple chat → ${r.cheap}`);
107
+ }
108
+ // 4) Default workhorse for tool/agent turns (and everything else): the tool-caller.
109
+ // Negative learning: if the workhorse has been unreliable for this intent and a
110
+ // better-scoring installed alternative exists, demote to it.
111
+ // Two-hop: a HARD tool turn first gets a plan from the reasoner (gated).
112
+ if (has(have, r.workhorse)) {
113
+ const wantTwoHop = process.env.DRAGON_ROUTER_TWOHOP === '1' && hasTools && has(have, r.reasoner) &&
114
+ (c.difficulty >= 0.5 || c.intent === 'reasoning' || c.intent === 'ops_security');
115
+ const withHop = (t) => {
116
+ if (wantTwoHop) {
117
+ t.twoHop = true;
118
+ t.reasoner = r.reasoner;
119
+ t.why += ` (+two-hop via ${r.reasoner})`;
120
+ }
121
+ return t;
122
+ };
123
+ const p = penalty(c.intent, r.workhorse);
124
+ if (p >= 0.7) {
125
+ const alt = [...have].find((m) => !/embed/.test(m) && m !== r.workhorse && m !== r.reasoner);
126
+ if (alt && penalty(c.intent, alt) < p) {
127
+ const t = local(alt, `routing-memory: ${r.workhorse} unreliable here (penalty ${p.toFixed(2)}) → ${alt}`);
128
+ t.penalty = penalty(c.intent, alt);
129
+ return withHop(t);
130
+ }
131
+ }
132
+ const t = local(r.workhorse, `${hasTools ? 'tool turn' : c.intent} → workhorse ${r.workhorse}`);
133
+ t.penalty = p;
134
+ return withHop(t);
135
+ }
136
+ // 5) Workhorse missing → degrade: any resident big local model, else Claude, else cheap.
137
+ // Exclude the reasoner (can't tool-call) so a tool turn never lands on it.
138
+ const usable = (m) => !/embed/.test(m) && m !== r.reasoner;
139
+ const fallback = [...live].find(usable) || [...have].find(usable);
140
+ if (fallback)
141
+ return local(fallback, `workhorse '${r.workhorse}' not installed → ${fallback}`);
142
+ if (claudeAvailable())
143
+ return { provider: 'claude', why: 'no local model available → Claude', swap: false };
144
+ return { provider: 'local', model: r.cheap, why: 'last-resort cheap local', swap: true };
145
+ }
146
+ //# sourceMappingURL=select.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Reason→tool two-hop (ROUTER-BLUEPRINT.md §1).
3
+ *
4
+ * vibethinker reasons brilliantly but CANNOT tool-call; mistral-nemo tool-calls but
5
+ * reasons less deeply. For a HARD tool turn the router splits the work:
6
+ * hop 1 — the reasoner produces a concrete PLAN (no tools), then
7
+ * hop 2 — the tool model executes that plan with the real tools.
8
+ *
9
+ * On 8 GB this is sequential (the two models can't co-reside) — Ollama swaps them,
10
+ * so it's gated behind DRAGON_ROUTER_TWOHOP=1 and only fires on hard tool turns.
11
+ *
12
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
13
+ */
14
+ import type { Brain, BrainTurn, TurnOpts } from '../types.js';
15
+ export interface TwoHopResult {
16
+ turn: BrainTurn;
17
+ planChars: number;
18
+ }
19
+ /**
20
+ * @param resolve factory to build the executor brain (from router/index.ts)
21
+ */
22
+ export declare function twoHop(localBaseURL: string, reasonerModel: string, workhorseModel: string, t: TurnOpts, resolve: (provider: string, model?: string) => Brain): Promise<TwoHopResult>;
23
+ //# sourceMappingURL=two-hop.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"two-hop.d.ts","sourceRoot":"","sources":["../../../src/brain/router/two-hop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAS7D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,SAAS,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;GAEG;AACH,wBAAsB,MAAM,CAC1B,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,EACtB,CAAC,EAAE,QAAQ,EACX,OAAO,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,KAAK,GACnD,OAAO,CAAC,YAAY,CAAC,CAwBvB"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Reason→tool two-hop (ROUTER-BLUEPRINT.md §1).
3
+ *
4
+ * vibethinker reasons brilliantly but CANNOT tool-call; mistral-nemo tool-calls but
5
+ * reasons less deeply. For a HARD tool turn the router splits the work:
6
+ * hop 1 — the reasoner produces a concrete PLAN (no tools), then
7
+ * hop 2 — the tool model executes that plan with the real tools.
8
+ *
9
+ * On 8 GB this is sequential (the two models can't co-reside) — Ollama swaps them,
10
+ * so it's gated behind DRAGON_ROUTER_TWOHOP=1 and only fires on hard tool turns.
11
+ *
12
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
13
+ */
14
+ import { ollamaChat, toOllamaMessages } from './verify.js';
15
+ const PLAN_SYSTEM = '\n\n── PLAN-ONLY MODE ──\n' +
16
+ 'You are the analyst. Read the user request and produce a brief, concrete, numbered ' +
17
+ 'PLAN to accomplish it (which tools to use, in what order, and why). Do NOT call any ' +
18
+ 'tools and do NOT write code blocks — output only the plan.';
19
+ /**
20
+ * @param resolve factory to build the executor brain (from router/index.ts)
21
+ */
22
+ export async function twoHop(localBaseURL, reasonerModel, workhorseModel, t, resolve) {
23
+ // hop 1 — reasoning/plan, no tools (so vibethinker never has to tool-call)
24
+ const plan = (await ollamaChat(localBaseURL, reasonerModel, toOllamaMessages(t.system + PLAN_SYSTEM, t.messages), 0.4, Math.min(t.maxTokens ?? 1024, 1024), t.signal)).trim();
25
+ // hop 2 — execution with the real tools, plan injected as guidance.
26
+ // Clean the plan (drop stray code fences) and cap it so the executor ACTS on it
27
+ // rather than engaging with it as prose, plus a firm act-now directive.
28
+ const cleanPlan = plan.replace(/```[\s\S]*?```/g, '').replace(/```/g, '').trim().slice(0, 700);
29
+ const enriched = cleanPlan
30
+ ? t.system +
31
+ '\n\n── PLAN (from the analyst) ──\n' + cleanPlan +
32
+ '\n\nExecute this plan NOW by calling the appropriate tool. Do NOT restate or ' +
33
+ 'explain the plan — issue the tool call.'
34
+ : t.system;
35
+ const worker = resolve('local', workhorseModel);
36
+ const turn = await worker.turn({ ...t, system: enriched });
37
+ return { turn, planChars: cleanPlan.length };
38
+ }
39
+ //# sourceMappingURL=two-hop.js.map
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Verified hard-reasoning path (ROUTER-BLUEPRINT.md §2).
3
+ *
4
+ * Ollama exposes no logits, so confidence = ANSWER-AGREEMENT: sample the reasoner
5
+ * N times at spread temperatures, extract each final answer, and majority-vote.
6
+ * The agreement ratio is the router's confidence signal (→ later: escalate if low).
7
+ * For code/security candidates we additionally run EXECUTION-based verification
8
+ * (see execute.ts) and use pass/fail as a hard reward.
9
+ *
10
+ * optillm: if DRAGON_OPTILLM_URL is set we treat it as a drop-in OpenAI-compatible
11
+ * test-time-scaling proxy and let IT do the scaling in one call (you run optillm
12
+ * pointed at Ollama). Otherwise we do best-of-N here — self-contained, no extra
13
+ * service, which suits the 8 GB local-first box.
14
+ *
15
+ * Copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
16
+ */
17
+ import type { BrainMessage, BrainTurn, TurnOpts } from '../types.js';
18
+ import { type ExecResult } from './execute.js';
19
+ export interface VerifyMeta {
20
+ via: 'self-consistency' | 'optillm';
21
+ votes: number;
22
+ agreement: number | null;
23
+ distribution?: Record<string, number>;
24
+ exec?: ExecResult | null;
25
+ }
26
+ export declare function toOllamaMessages(system: string, messages: BrainMessage[]): {
27
+ role: string;
28
+ content: string;
29
+ }[];
30
+ export declare function ollamaChat(base: string, model: string, messages: unknown, temperature: number, maxTokens: number, signal?: AbortSignal): Promise<string>;
31
+ export declare function extractAnswer(text: string): string | null;
32
+ /** Run the reasoner with test-time scaling. Returns the chosen turn + how confident. */
33
+ export declare function verifyReasoning(localBaseURL: string, model: string, t: TurnOpts, votes: number): Promise<{
34
+ turn: BrainTurn;
35
+ meta: VerifyMeta;
36
+ }>;
37
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../../src/brain/router/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AACpE,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAA;AAE7D,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,kBAAkB,GAAG,SAAS,CAAA;IACnC,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACrC,IAAI,CAAC,EAAE,UAAU,GAAG,IAAI,CAAA;CACzB;AAID,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE;UACpD,MAAM;aAAW,MAAM;IAM3C;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAU9J;AAcD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOzD;AAID,wFAAwF;AACxF,wBAAsB,eAAe,CACnC,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,CAAC,EAAE,QAAQ,EACX,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC,CA0ChD"}