nemoris 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/.env.example +49 -49
  2. package/LICENSE +21 -21
  3. package/README.md +209 -209
  4. package/SECURITY.md +59 -119
  5. package/bin/nemoris +46 -46
  6. package/config/agents/agent.toml.example +28 -28
  7. package/config/agents/content.toml +23 -0
  8. package/config/agents/default.toml +22 -22
  9. package/config/agents/heartbeat.toml +35 -0
  10. package/config/agents/iris.toml +23 -0
  11. package/config/agents/lab.toml +23 -0
  12. package/config/agents/main.toml +45 -0
  13. package/config/agents/nemo.toml +21 -0
  14. package/config/agents/ops.toml +38 -0
  15. package/config/agents/orchestrator.toml +18 -18
  16. package/config/agents/revenue.toml +23 -0
  17. package/config/agents/testyboo.toml +19 -0
  18. package/config/delivery.toml +73 -73
  19. package/config/embeddings.toml +5 -5
  20. package/config/identity/content-purpose.md +11 -0
  21. package/config/identity/content-soul.md +45 -0
  22. package/config/identity/default-purpose.md +1 -1
  23. package/config/identity/default-soul.md +3 -3
  24. package/config/identity/heartbeat-purpose.md +9 -0
  25. package/config/identity/heartbeat-soul.md +16 -0
  26. package/config/identity/iris-purpose.md +17 -0
  27. package/config/identity/iris-soul.md +68 -0
  28. package/config/identity/lab-purpose.md +10 -0
  29. package/config/identity/lab-soul.md +38 -0
  30. package/config/identity/main-purpose.md +17 -0
  31. package/config/identity/main-soul.md +66 -0
  32. package/config/identity/main-user.md +22 -0
  33. package/config/identity/ops-purpose.md +9 -0
  34. package/config/identity/ops-soul.md +16 -0
  35. package/config/identity/orchestrator-purpose.md +1 -1
  36. package/config/identity/orchestrator-soul.md +1 -1
  37. package/config/identity/revenue-purpose.md +9 -0
  38. package/config/identity/revenue-soul.md +41 -0
  39. package/config/identity/testyboo-purpose.md +13 -0
  40. package/config/identity/testyboo-soul.md +20 -0
  41. package/config/improvement-targets.toml +15 -15
  42. package/config/jobs/heartbeat-check.toml +30 -30
  43. package/config/jobs/memory-rollup.toml +46 -46
  44. package/config/jobs/workspace-health.toml +63 -63
  45. package/config/mcp.toml +16 -16
  46. package/config/output-contracts.toml +17 -17
  47. package/config/peers.toml +32 -32
  48. package/config/peers.toml.example +32 -32
  49. package/config/policies/memory-default.toml +10 -10
  50. package/config/policies/memory-heartbeat.toml +5 -5
  51. package/config/policies/memory-ops.toml +10 -10
  52. package/config/policies/tools-heartbeat-minimal.toml +8 -8
  53. package/config/policies/tools-interactive-safe.toml +8 -8
  54. package/config/policies/tools-ops-bounded.toml +8 -8
  55. package/config/policies/tools-orchestrator.toml +7 -7
  56. package/config/providers/anthropic.toml +15 -15
  57. package/config/providers/ollama.toml +5 -5
  58. package/config/providers/openai-codex.toml +9 -9
  59. package/config/providers/openrouter.toml +5 -5
  60. package/config/router.toml +22 -22
  61. package/config/runtime.toml +114 -114
  62. package/config/skills/self-improvement.toml +15 -15
  63. package/config/skills/telegram-onboarding-spec.md +240 -240
  64. package/config/skills/workspace-monitor.toml +15 -15
  65. package/config/task-router.toml +42 -42
  66. package/install.sh +50 -50
  67. package/package.json +91 -90
  68. package/src/auth/auth-profiles.js +169 -169
  69. package/src/auth/openai-codex-oauth.js +285 -285
  70. package/src/battle.js +449 -449
  71. package/src/cli/help.js +265 -265
  72. package/src/cli/output-filter.js +49 -49
  73. package/src/cli/runtime-control.js +704 -704
  74. package/src/cli-main.js +2763 -2763
  75. package/src/cli.js +78 -78
  76. package/src/config/loader.js +332 -332
  77. package/src/config/schema-validator.js +214 -214
  78. package/src/config/toml-lite.js +8 -8
  79. package/src/daemon/action-handlers.js +71 -71
  80. package/src/daemon/healing-tick.js +87 -87
  81. package/src/daemon/health-probes.js +90 -90
  82. package/src/daemon/notifier.js +57 -57
  83. package/src/daemon/nurse.js +218 -218
  84. package/src/daemon/repair-log.js +106 -106
  85. package/src/daemon/rule-staging.js +90 -90
  86. package/src/daemon/rules.js +29 -29
  87. package/src/daemon/telegram-commands.js +54 -54
  88. package/src/daemon/updater.js +85 -85
  89. package/src/jobs/job-runner.js +78 -78
  90. package/src/mcp/consumer.js +129 -129
  91. package/src/memory/active-recall.js +171 -171
  92. package/src/memory/backend-manager.js +97 -97
  93. package/src/memory/backends/file-backend.js +38 -38
  94. package/src/memory/backends/qmd-backend.js +219 -219
  95. package/src/memory/embedding-guards.js +24 -24
  96. package/src/memory/embedding-index.js +118 -118
  97. package/src/memory/embedding-service.js +179 -179
  98. package/src/memory/file-index.js +177 -177
  99. package/src/memory/memory-signature.js +5 -5
  100. package/src/memory/memory-store.js +648 -648
  101. package/src/memory/retrieval-planner.js +66 -66
  102. package/src/memory/scoring.js +145 -145
  103. package/src/memory/simhash.js +78 -78
  104. package/src/memory/sqlite-active-store.js +824 -824
  105. package/src/memory/write-policy.js +36 -36
  106. package/src/onboarding/aliases.js +33 -33
  107. package/src/onboarding/auth/api-key.js +224 -224
  108. package/src/onboarding/auth/ollama-detect.js +42 -42
  109. package/src/onboarding/clack-prompter.js +77 -77
  110. package/src/onboarding/doctor.js +530 -530
  111. package/src/onboarding/lock.js +42 -42
  112. package/src/onboarding/model-catalog.js +344 -344
  113. package/src/onboarding/phases/auth.js +576 -589
  114. package/src/onboarding/phases/build.js +130 -130
  115. package/src/onboarding/phases/choose.js +82 -82
  116. package/src/onboarding/phases/detect.js +98 -98
  117. package/src/onboarding/phases/hatch.js +216 -216
  118. package/src/onboarding/phases/identity.js +79 -79
  119. package/src/onboarding/phases/ollama.js +345 -345
  120. package/src/onboarding/phases/scaffold.js +99 -99
  121. package/src/onboarding/phases/telegram.js +377 -377
  122. package/src/onboarding/phases/validate.js +204 -204
  123. package/src/onboarding/phases/verify.js +206 -206
  124. package/src/onboarding/platform.js +482 -482
  125. package/src/onboarding/status-bar.js +95 -95
  126. package/src/onboarding/templates.js +794 -794
  127. package/src/onboarding/toml-writer.js +38 -38
  128. package/src/onboarding/tui.js +250 -250
  129. package/src/onboarding/uninstall.js +153 -153
  130. package/src/onboarding/wizard.js +516 -499
  131. package/src/providers/anthropic.js +168 -168
  132. package/src/providers/base.js +247 -247
  133. package/src/providers/circuit-breaker.js +136 -136
  134. package/src/providers/ollama.js +163 -163
  135. package/src/providers/openai-codex.js +149 -149
  136. package/src/providers/openrouter.js +136 -136
  137. package/src/providers/registry.js +36 -36
  138. package/src/providers/router.js +16 -16
  139. package/src/runtime/bootstrap-cache.js +47 -47
  140. package/src/runtime/capabilities-prompt.js +25 -25
  141. package/src/runtime/completion-ping.js +99 -99
  142. package/src/runtime/config-validator.js +121 -121
  143. package/src/runtime/context-ledger.js +360 -360
  144. package/src/runtime/cutover-readiness.js +42 -42
  145. package/src/runtime/daemon.js +729 -729
  146. package/src/runtime/delivery-ack.js +195 -195
  147. package/src/runtime/delivery-adapters/local-file.js +41 -41
  148. package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
  149. package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
  150. package/src/runtime/delivery-adapters/shadow.js +13 -13
  151. package/src/runtime/delivery-adapters/standalone-http.js +98 -98
  152. package/src/runtime/delivery-adapters/telegram.js +104 -104
  153. package/src/runtime/delivery-adapters/tui.js +128 -128
  154. package/src/runtime/delivery-manager.js +807 -807
  155. package/src/runtime/delivery-store.js +168 -168
  156. package/src/runtime/dependency-health.js +118 -118
  157. package/src/runtime/envelope.js +114 -114
  158. package/src/runtime/evaluation.js +1089 -1089
  159. package/src/runtime/exec-approvals.js +216 -216
  160. package/src/runtime/executor.js +500 -500
  161. package/src/runtime/failure-ping.js +67 -67
  162. package/src/runtime/flows.js +83 -83
  163. package/src/runtime/guards.js +45 -45
  164. package/src/runtime/handoff.js +51 -51
  165. package/src/runtime/identity-cache.js +28 -28
  166. package/src/runtime/improvement-engine.js +109 -109
  167. package/src/runtime/improvement-harness.js +581 -581
  168. package/src/runtime/input-sanitiser.js +72 -72
  169. package/src/runtime/interaction-contract.js +347 -347
  170. package/src/runtime/lane-readiness.js +226 -226
  171. package/src/runtime/migration.js +323 -323
  172. package/src/runtime/model-resolution.js +78 -78
  173. package/src/runtime/network.js +64 -64
  174. package/src/runtime/notification-store.js +97 -97
  175. package/src/runtime/notifier.js +256 -256
  176. package/src/runtime/orchestrator.js +53 -53
  177. package/src/runtime/orphan-reaper.js +41 -41
  178. package/src/runtime/output-contract-schema.js +139 -139
  179. package/src/runtime/output-contract-validator.js +439 -439
  180. package/src/runtime/peer-readiness.js +69 -69
  181. package/src/runtime/peer-registry.js +133 -133
  182. package/src/runtime/pilot-status.js +108 -108
  183. package/src/runtime/prompt-builder.js +261 -261
  184. package/src/runtime/provider-attempt.js +582 -582
  185. package/src/runtime/report-fallback.js +71 -71
  186. package/src/runtime/result-normalizer.js +183 -183
  187. package/src/runtime/retention.js +74 -74
  188. package/src/runtime/review.js +244 -244
  189. package/src/runtime/route-job.js +15 -15
  190. package/src/runtime/run-store.js +38 -38
  191. package/src/runtime/schedule.js +88 -88
  192. package/src/runtime/scheduler-state.js +434 -434
  193. package/src/runtime/scheduler.js +656 -656
  194. package/src/runtime/session-compactor.js +182 -182
  195. package/src/runtime/session-search.js +155 -155
  196. package/src/runtime/slack-inbound.js +249 -249
  197. package/src/runtime/ssrf.js +102 -102
  198. package/src/runtime/status-aggregator.js +330 -330
  199. package/src/runtime/task-contract.js +140 -140
  200. package/src/runtime/task-packet.js +107 -107
  201. package/src/runtime/task-router.js +140 -140
  202. package/src/runtime/telegram-inbound.js +1565 -1565
  203. package/src/runtime/token-counter.js +134 -134
  204. package/src/runtime/token-estimator.js +59 -59
  205. package/src/runtime/tool-loop.js +200 -200
  206. package/src/runtime/transport-server.js +311 -311
  207. package/src/runtime/tui-server.js +411 -411
  208. package/src/runtime/ulid.js +44 -44
  209. package/src/security/ssrf-check.js +197 -197
  210. package/src/setup.js +369 -369
  211. package/src/shadow/bridge.js +303 -303
  212. package/src/skills/loader.js +84 -84
  213. package/src/tools/catalog.json +49 -49
  214. package/src/tools/cli-delegate.js +44 -44
  215. package/src/tools/mcp-client.js +106 -106
  216. package/src/tools/micro/cancel-task.js +6 -6
  217. package/src/tools/micro/complete-task.js +6 -6
  218. package/src/tools/micro/fail-task.js +6 -6
  219. package/src/tools/micro/http-fetch.js +74 -74
  220. package/src/tools/micro/index.js +36 -36
  221. package/src/tools/micro/lcm-recall.js +60 -60
  222. package/src/tools/micro/list-dir.js +17 -17
  223. package/src/tools/micro/list-skills.js +46 -46
  224. package/src/tools/micro/load-skill.js +38 -38
  225. package/src/tools/micro/memory-search.js +45 -45
  226. package/src/tools/micro/read-file.js +11 -11
  227. package/src/tools/micro/session-search.js +54 -54
  228. package/src/tools/micro/shell-exec.js +43 -43
  229. package/src/tools/micro/trigger-job.js +79 -79
  230. package/src/tools/micro/web-search.js +58 -58
  231. package/src/tools/micro/workspace-paths.js +39 -39
  232. package/src/tools/micro/write-file.js +14 -14
  233. package/src/tools/micro/write-memory.js +41 -41
  234. package/src/tools/registry.js +348 -348
  235. package/src/tools/tool-result-contract.js +36 -36
  236. package/src/tui/chat.js +835 -835
  237. package/src/tui/renderer.js +175 -175
  238. package/src/tui/socket-client.js +217 -217
  239. package/src/utils/canonical-json.js +29 -29
  240. package/src/utils/compaction.js +30 -30
  241. package/src/utils/env-loader.js +5 -5
  242. package/src/utils/errors.js +80 -80
  243. package/src/utils/fs.js +101 -101
  244. package/src/utils/ids.js +5 -5
  245. package/src/utils/model-context-limits.js +30 -30
  246. package/src/utils/token-budget.js +74 -74
  247. package/src/utils/usage-cost.js +25 -25
  248. package/src/utils/usage-metrics.js +14 -14
@@ -1,794 +1,794 @@
1
- /**
2
- * Bundled TOML and markdown templates for the onboarding scaffold phase.
3
- * Each function accepts injected values and returns file content as a string.
4
- */
5
-
6
- import path from "node:path";
7
- import { writeFile as writeFileAsync, mkdir as mkdirAsync, access as accessAsync } from "node:fs/promises";
8
-
9
- // ── Agent manifest ───────────────────────────────────────────────────────────
10
-
11
- /**
12
- * Generates config/agents/{agentId}.toml
13
- * Must satisfy AGENT_SCHEMA: id, primaryLane, memoryPolicy, toolPolicy
14
- */
15
- export function agentTemplate({ agentId, installDir = "", workspaceRoot = "" }) {
16
- // Normalise to forward slashes — backslashes break TOML string parsing (\U etc.)
17
- const safeInstallDir = installDir.replace(/\\/g, "/");
18
- const identityBase = safeInstallDir
19
- ? `${safeInstallDir}/config/identity`
20
- : `config/identity`;
21
- const wsRoot = (workspaceRoot || (safeInstallDir ? `${safeInstallDir}/workspace` : "workspace")).replace(/\\/g, "/");
22
-
23
- return `id = "${agentId}"
24
- primary_lane = "interactive_primary"
25
- memory_policy = "default"
26
- tool_policy = "interactive_safe"
27
- soul_ref = "${identityBase}/${agentId}-soul.md"
28
- purpose_ref = "${identityBase}/${agentId}-purpose.md"
29
- workspace_root = "${wsRoot}"
30
- workspace_context_files = ["MEMORY.md", "USER.md", "AGENTS.md"]
31
- workspace_context_cap = 8000
32
- checkpoint_policy = "compact"
33
-
34
- [limits]
35
- max_tokens_per_turn = 16000
36
- max_tool_calls_per_turn = 6
37
- max_runtime_seconds = 120
38
-
39
- [access]
40
- workspace = "rw"
41
- network = "restricted"
42
- `;
43
- }
44
-
45
- // ── Identity files ───────────────────────────────────────────────────────────
46
-
47
- /**
48
- * Generates config/identity/{agentId}-soul.md
49
- */
50
- export function soulTemplate({ agentName, userName, date }) {
51
- return `# Soul — ${agentName}
52
-
53
- Created: ${date}
54
- Operator: ${userName}
55
-
56
- You are a pragmatic technical coworker assisting ${userName}.
57
-
58
- Values:
59
-
60
- - clarity over performance theater
61
- - durable systems over clever hacks
62
- - truthful reporting over optimistic guessing
63
- - calm execution under ambiguity
64
-
65
- Behavior:
66
-
67
- - speak directly
68
- - keep context lean
69
- - document what matters
70
- - protect ${userName}'s trust and data
71
- `;
72
- }
73
-
74
- /**
75
- * Generates config/identity/{agentId}-purpose.md
76
- */
77
- export function purposeTemplate({ agentName, userName, userGoal, date }) {
78
- return `# Purpose — ${agentName}
79
-
80
- Created: ${date}
81
- Operator: ${userName}
82
-
83
- Help ${userName} ${userGoal}.
84
-
85
- Primary responsibilities:
86
-
87
- - drive implementation forward
88
- - preserve continuity across sessions
89
- - surface risk early
90
- - convert messy state into clear action
91
- `;
92
- }
93
-
94
- // ── Router ───────────────────────────────────────────────────────────────────
95
-
96
- /**
97
- * Generates config/router.toml with lanes based on available providers.
98
- * Must satisfy ROUTER_SCHEMA: minLanes = 1
99
- */
100
- export function routerTemplate({ anthropic = false, openrouter = false, openai = false, ollama = false, selectedModels = {} }) {
101
- const pick = (provider, defaults = []) => {
102
- const chosen = Array.isArray(selectedModels?.[provider]) && selectedModels[provider].length > 0
103
- ? selectedModels[provider]
104
- : defaults;
105
- return {
106
- primary: chosen[0] || defaults[0],
107
- secondary: chosen[1] || defaults[1] || chosen[0] || defaults[0],
108
- tertiary: chosen[2] || defaults[2] || chosen[1] || defaults[1] || chosen[0] || defaults[0],
109
- };
110
- };
111
-
112
- const anthropicModels = pick("anthropic", [
113
- "anthropic/claude-haiku-4-5",
114
- "anthropic/claude-sonnet-4-6",
115
- "anthropic/claude-opus-4-6",
116
- ]);
117
- const openrouterModels = pick("openrouter", [
118
- "openrouter/anthropic/claude-haiku-4-5",
119
- "openrouter/anthropic/claude-sonnet-4-6",
120
- "openrouter/anthropic/claude-opus-4-6",
121
- ]);
122
- const openaiModels = pick("openai", [
123
- "openai-codex/gpt-4.1",
124
- "openai-codex/gpt-4o",
125
- "openai-codex/o4-mini",
126
- ]);
127
- const ollamaModels = pick("ollama", [
128
- "ollama/qwen2.5:0.5b",
129
- "ollama/qwen3:8b",
130
- "ollama/qwen3:14b",
131
- ]);
132
-
133
- const sections = [];
134
-
135
- // interactive_primary lane — must always exist
136
- if (openrouter && anthropic) {
137
- sections.push(`[lanes.interactive_primary]
138
- primary = "${openrouterModels.primary}"
139
- fallback = "${anthropicModels.primary}"
140
- manual_bump = "${openrouterModels.secondary}"`);
141
- } else if (openrouter) {
142
- sections.push(`[lanes.interactive_primary]
143
- primary = "${openrouterModels.primary}"
144
- manual_bump = "${openrouterModels.secondary}"`);
145
- } else if (anthropic) {
146
- sections.push(`[lanes.interactive_primary]
147
- primary = "${anthropicModels.primary}"
148
- manual_bump = "${anthropicModels.secondary}"`);
149
- } else if (openai) {
150
- sections.push(`[lanes.interactive_primary]
151
- primary = "${openaiModels.primary}"
152
- manual_bump = "${openaiModels.secondary}"`);
153
- } else if (ollama) {
154
- // ollama-only fallback for interactive
155
- sections.push(`[lanes.interactive_primary]
156
- primary = "${ollamaModels.secondary}"`);
157
- } else {
158
- // No providers at all — emit a placeholder lane so router.toml satisfies minLanes = 1
159
- sections.push(`# No providers configured — add one with: nemoris setup
160
- [lanes.interactive_primary]
161
- primary = "none"`);
162
- }
163
-
164
- // local lanes — only if ollama is available
165
- if (ollama) {
166
- sections.push(`[lanes.local_cheap]
167
- primary = "${ollamaModels.primary}"`);
168
-
169
- if (openrouter) {
170
- sections.push(`[lanes.local_report]
171
- primary = "${ollamaModels.secondary}"
172
- fallback = "${openrouterModels.primary}"
173
- manual_bump = "${ollamaModels.tertiary}"`);
174
- } else if (openai) {
175
- sections.push(`[lanes.local_report]
176
- primary = "${ollamaModels.secondary}"
177
- fallback = "${openaiModels.primary}"
178
- manual_bump = "${ollamaModels.tertiary}"`);
179
- } else {
180
- sections.push(`[lanes.local_report]
181
- primary = "${ollamaModels.secondary}"
182
- manual_bump = "${ollamaModels.tertiary}"`);
183
- }
184
- }
185
-
186
- // report_fallback_lowcost — only if remote providers are available
187
- if (openrouter) {
188
- sections.push(`[lanes.report_fallback_lowcost]
189
- primary = "${openrouterModels.primary}"`);
190
- } else if (openai) {
191
- sections.push(`[lanes.report_fallback_lowcost]
192
- primary = "${openaiModels.primary}"`);
193
- }
194
-
195
- // job_heavy — only if cloud providers available
196
- if (openrouter && anthropic) {
197
- sections.push(`[lanes.job_heavy]
198
- primary = "${openrouterModels.secondary}"
199
- fallback = "${anthropicModels.secondary}"`);
200
- } else if (openrouter) {
201
- sections.push(`[lanes.job_heavy]
202
- primary = "${openrouterModels.secondary}"`);
203
- } else if (anthropic) {
204
- sections.push(`[lanes.job_heavy]
205
- primary = "${anthropicModels.secondary}"`);
206
- } else if (openai) {
207
- sections.push(`[lanes.job_heavy]
208
- primary = "${openaiModels.secondary}"`);
209
- }
210
-
211
- return sections.join("\n\n") + "\n";
212
- }
213
-
214
- // ── Runtime ──────────────────────────────────────────────────────────────────
215
-
216
- /**
217
- * Generates config/runtime.toml with shipped defaults.
218
- * Must satisfy RUNTIME_SCHEMA: safety.contextTokens
219
- */
220
- export function runtimeTemplate() {
221
- return `[safety]
222
- context_tokens = 32768
223
- context_pressure_soft_ratio = 0.72
224
- context_pressure_hard_ratio = 0.9
225
- fresh_session_on_high_pressure = true
226
- snapshot_before_compaction = true
227
-
228
- [concurrency]
229
- max_concurrent_jobs = 2
230
- max_concurrent_subagents = 2
231
-
232
- [retention.runs]
233
- ttl_days = 30
234
- max_files_per_bucket = 2000
235
-
236
- [retention.notifications]
237
- ttl_days = 14
238
- max_files_per_bucket = 1000
239
-
240
- [retention.deliveries]
241
- ttl_days = 14
242
- max_files_per_bucket = 1000
243
-
244
- [retention.transport_inbox]
245
- ttl_days = 7
246
- max_files_per_bucket = 1000
247
-
248
- [retrieval]
249
- lexical_weight = 0.36
250
- embedding_weight = 0.3
251
- recency_weight = 0.14
252
- salience_weight = 0.14
253
- type_weight = 0.06
254
- semantic_rescue_bonus = 0.06
255
- shadow_snapshot_penalty = 0.12
256
-
257
- [memory_locks]
258
- ttl_ms = 15000
259
- retry_delay_ms = 25
260
- max_retries = 40
261
-
262
- [network]
263
- dns_result_order = "system"
264
- connect_timeout_ms = 20000
265
- read_timeout_ms = 60000
266
- retry_budget = 1
267
- circuit_breaker_threshold = 3
268
-
269
- [bootstrap_cache]
270
- enabled = true
271
- identity_ttl_ms = 300000
272
-
273
- [extensions]
274
- implicit_workspace_autoload = false
275
- require_explicit_trust = true
276
- trusted_roots = []
277
-
278
- [shutdown]
279
- drain_timeout_ms = 15000
280
- transport_shutdown_timeout_ms = 5000
281
-
282
- [maintenance]
283
- wal_checkpoint_threshold_bytes = 67108864
284
- prune_on_tick = true
285
- sweep_pending_handoffs_on_tick = true
286
- sweep_pending_followups_on_tick = true
287
-
288
- [delivery]
289
- prevent_resend_on_uncertain = true
290
- retry_on_failure = false
291
- notify_on_failure = true
292
-
293
- [yields]
294
- enabled = true
295
- default_target_surface = "operator_review"
296
- `;
297
- }
298
-
299
- // ── Delivery ─────────────────────────────────────────────────────────────────
300
-
301
- /**
302
- * Generates a minimal config/delivery.toml with standalone profiles.
303
- */
304
- export function deliveryTemplate() {
305
- return `default_interactive_profile_standalone = "standalone_operator"
306
- default_peer_profile_standalone = "standalone_peer"
307
-
308
- [profiles.shadow_scheduler]
309
- adapter = "shadow"
310
- enabled = true
311
- target = "scheduler_log"
312
-
313
- [profiles.standalone_operator]
314
- adapter = "local_file"
315
- enabled = true
316
- target = "operator"
317
-
318
- [profiles.standalone_peer]
319
- adapter = "local_file"
320
- enabled = true
321
- target = "peer_queue"
322
- `;
323
- }
324
-
325
- // ── Peers ────────────────────────────────────────────────────────────────────
326
-
327
- /**
328
- * Generates an empty config/peers.toml scaffold.
329
- */
330
- export function peersTemplate() {
331
- return `# Peer agent registry
332
- # Add peers here as you connect additional agents.
333
- # See config/peers.toml in the reference installation for the full format.
334
- `;
335
- }
336
-
337
- // ── Output contracts ─────────────────────────────────────────────────────────
338
-
339
- /**
340
- * Generates config/output-contracts.toml with shipped profiles.
341
- */
342
- export function outputContractsTemplate() {
343
- return `[profiles.default]
344
- require_status = true
345
- section_style = "freeform"
346
- require_section_items = false
347
- template_lines = ["Status: <one-line status>", "<response body>"]
348
-
349
- [profiles.bulleted_briefing]
350
- require_status = true
351
- section_style = "bullets"
352
- require_section_items = true
353
- template_lines = ["Status: <one-line status>", "- Calendar: <brief update or None>", "- Issues: <brief update or None>", "- Weather: <brief update or None>"]
354
-
355
- [profiles.structured_rollup]
356
- require_status = false
357
- section_style = "headings"
358
- require_section_items = true
359
- template_lines = ["## Inbox", "- <brief update or None>", "", "## Projects", "- <brief update or None>", "", "## Backlog", "- <brief update or None>", "", "## Update", "- <brief update or None>"]
360
- `;
361
- }
362
-
363
- // ── Embeddings ───────────────────────────────────────────────────────────────
364
-
365
- /**
366
- * Generates config/embeddings.toml with embeddings disabled by default.
367
- */
368
- export function embeddingsTemplate() {
369
- return `enabled = true
370
- provider = "ollama"
371
- model = "ollama/nomic-embed-text"
372
- dimensions = 128
373
- index_on_write = true
374
- `;
375
- }
376
-
377
- // ── Improvement targets ──────────────────────────────────────────────────────
378
-
379
- /**
380
- * Generates an empty config/improvement-targets.toml scaffold.
381
- */
382
- export function improvementTargetsTemplate() {
383
- return `# Improvement targets — add entries here to enable the improvement harness.
384
- # See config/improvement-targets.toml in the reference installation for format.
385
- `;
386
- }
387
-
388
- // ── Provider ─────────────────────────────────────────────────────────────────
389
-
390
- /**
391
- * Generates config/providers/{id}.toml
392
- * Must satisfy PROVIDER_SCHEMA: id, and at least one of adapter/type
393
- */
394
- export function providerTemplate(providerId, config = {}) {
395
- const {
396
- adapter = providerId,
397
- authEnv = `${providerId.toUpperCase()}_API_KEY`,
398
- authRef = authEnv ? `env:${authEnv}` : "",
399
- baseUrl = "",
400
- healthcheck = "",
401
- timeoutMs = 45000,
402
- lanes = [],
403
- models = []
404
- } = config;
405
-
406
- let lines = [`id = "${providerId}"`, `adapter = "${adapter}"`];
407
-
408
- if (authRef) {
409
- lines.push(`auth_ref = "${authRef}"`);
410
- }
411
- if (baseUrl) {
412
- lines.push(`base_url = "${baseUrl}"`);
413
- }
414
- if (healthcheck) {
415
- lines.push(`healthcheck = "${healthcheck}"`);
416
- }
417
- lines.push(`default_timeout_ms = ${timeoutMs}`);
418
- if (lanes.length > 0) {
419
- lines.push(`lanes = [${lanes.map((l) => `"${l}"`).join(", ")}]`);
420
- }
421
-
422
- const body = lines.join("\n");
423
-
424
- const modelSections = models
425
- .map(({ key, id, role }) => `\n[models.${key}]\nid = "${id}"\nrole = "${role}"`)
426
- .join("\n");
427
-
428
- return body + "\n" + modelSections + (modelSections ? "\n" : "");
429
- }
430
-
431
- // ── Job ──────────────────────────────────────────────────────────────────────
432
-
433
- /**
434
- * Generates config/jobs/{id}.toml — workspace-health default.
435
- * Must satisfy JOB_SCHEMA: id, modelLane, and at least one of schedule/trigger
436
- */
437
- export function jobTemplate(jobId, { agentId = "nemo" } = {}) {
438
- return `id = "${jobId}"
439
- agent_id = "${agentId}"
440
- trigger = "hourly"
441
- task_type = "workspace_health"
442
- model_lane = "local_report"
443
- output_target = "state/health"
444
- idempotency_key = "${jobId}:hour"
445
- memory_backends = ["file"]
446
- memory_limit = 6
447
-
448
- [budget]
449
- max_tokens = 4000
450
- max_runtime_seconds = 120
451
-
452
- [retry]
453
- max_attempts = 1
454
-
455
- [stop]
456
- halt_on_policy_error = true
457
- halt_on_budget_exceeded = true
458
- `;
459
- }
460
-
461
- // ── Policy files ─────────────────────────────────────────────────────────────
462
-
463
- /**
464
- * Generates default policy file content keyed by filename stem.
465
- * Returns an object: { [filename]: tomlString }
466
- */
467
- export function policyTemplates() {
468
- return {
469
- "memory-default": `id = "default"
470
- allow_durable_writes = true
471
- allow_identity_updates = false
472
- require_source_reference = true
473
- require_write_reason = true
474
- max_writes_per_run = 5
475
-
476
- [categories]
477
- allowed = ["decision", "preference", "workflow_rule", "artifact_summary"]
478
- blocked = ["ephemeral_chatter", "raw_tool_output", "unverified_external_claim"]
479
- `,
480
- "tools-interactive-safe": `id = "interactive_safe"
481
- default = "deny"
482
- allowed = ["read_file", "search_file", "list_dir", "apply_patch", "run_tests"]
483
- blocked = ["send_email", "post_web", "delete_file", "reset_repo"]
484
-
485
- [limits]
486
- max_parallel = 3
487
- require_approval_for_network = true
488
- `,
489
- "tools-ops-bounded": `id = "ops_bounded"
490
- default = "deny"
491
- allowed = ["read_file", "search_file", "list_dir", "apply_patch", "check_status", "run_tests"]
492
- blocked = ["send_email", "post_web", "delete_file", "reset_repo"]
493
-
494
- [limits]
495
- max_parallel = 3
496
- require_approval_for_network = true
497
- `
498
- };
499
- }
500
-
501
- // ── Env file ─────────────────────────────────────────────────────────────────
502
-
503
- /**
504
- * Generates .env content from a key-value pairs object.
505
- * @param {Record<string, string>} keys
506
- */
507
- export function envTemplate(keys = {}) {
508
- const header = `# nemoris environment variables\n# Generated by onboarding wizard\n`;
509
- const lines = Object.entries(keys)
510
- .map(([k, v]) => `${k}=${v}`)
511
- .join("\n");
512
- return header + (lines ? lines + "\n" : "");
513
- }
514
-
515
- // ── Execution layer scaffold ──────────────────────────────────────────────────
516
-
517
- /**
518
- * Creates config/tools, config/skills, config/agents, config/policies
519
- * directories and writes default skill manifests and orchestrator config.
520
- * Safe to run multiple times (directories use recursive: true).
521
- *
522
- * @param {string} installDir Absolute path to the nemoris installation root.
523
- */
524
- export async function scaffoldToolsAndSkills(installDir) {
525
- const toolsDir = path.join(installDir, "config", "tools");
526
- const skillsDir = path.join(installDir, "config", "skills");
527
- const agentsDir = path.join(installDir, "config", "agents");
528
- const policiesDir = path.join(installDir, "config", "policies");
529
-
530
- await mkdirAsync(toolsDir, { recursive: true });
531
- await mkdirAsync(skillsDir, { recursive: true });
532
- await mkdirAsync(agentsDir, { recursive: true });
533
- await mkdirAsync(policiesDir, { recursive: true });
534
-
535
- // Default skills
536
- await writeFileAsync(path.join(skillsDir, "workspace-monitor.toml"), `[skill]
537
- id = "workspace_monitor"
538
- description = "Monitor workspace directory for changes and summarise"
539
- agent_scope = ["ops", "main"]
540
-
541
- [skill.context]
542
- prompt = "You are monitoring a workspace directory for changes. Compare current state against last known checkpoint. Report: new files, modified files, deleted files, key content changes. Keep summary under 200 words."
543
-
544
- [skill.tools]
545
- required = ["read_file", "list_dir"]
546
- optional = ["shell_exec"]
547
-
548
- [skill.budget]
549
- max_tokens = 4096
550
- max_tool_calls = 10
551
- `);
552
-
553
- await writeFileAsync(path.join(skillsDir, "self-improvement.toml"), `[skill]
554
- id = "self_improvement"
555
- description = "Analyse run artifacts and apply tuning adjustments"
556
- agent_scope = ["ops", "main"]
557
-
558
- [skill.context]
559
- prompt = "You are reviewing a run artifact that scored below threshold. Read the run artifact and current tunings. Identify the root cause. Apply ONE of: prompt refinement, budget adjustment, tool policy change, lane escalation. Write the tuning to state/tunings/<jobId>/ as a JSON file. Explain what you changed and why in under 100 words."
560
-
561
- [skill.tools]
562
- required = ["read_file", "write_file", "list_dir"]
563
- optional = []
564
-
565
- [skill.budget]
566
- max_tokens = 4096
567
- max_tool_calls = 8
568
- `);
569
-
570
- // Orchestrator agent
571
- await writeFileAsync(path.join(agentsDir, "orchestrator.toml"), `id = "orchestrator"
572
- soul_ref = "config/identity/orchestrator-soul.md"
573
- purpose_ref = "config/identity/orchestrator-purpose.md"
574
- primary_lane = "local_cheap"
575
- fallback_lane = "interactive_fallback"
576
- tool_policy = "orchestrator"
577
- memory_policy = "orchestrator"
578
-
579
- [limits]
580
- max_tokens_per_turn = 2700
581
-
582
- [routing.static]
583
- "heartbeat-check" = "heartbeat"
584
-
585
- [routing.dynamic]
586
- enabled = true
587
- model_lane = "local_cheap"
588
- max_routing_tokens = 500
589
- `);
590
-
591
- // Orchestrator identity files
592
- const identityDir = path.join(installDir, "config", "identity");
593
- await mkdirAsync(identityDir, { recursive: true });
594
- const orchSoulPath = path.join(identityDir, "orchestrator-soul.md");
595
- const orchPurposePath = path.join(identityDir, "orchestrator-purpose.md");
596
- // Only write if missing (don't overwrite user edits)
597
- try { await accessAsync(orchSoulPath); } catch {
598
- await writeFileAsync(orchSoulPath, `You are the orchestrator — a routing and delegation agent.\nYou do not execute tasks directly. You inspect incoming work, select the best agent, and delegate.\nBe concise. Favour the cheapest capable lane. Escalate only when the task demands it.\n`);
599
- }
600
- try { await accessAsync(orchPurposePath); } catch {
601
- await writeFileAsync(orchPurposePath, `Route incoming jobs to the correct agent and model lane.\nMinimise cost. Maximise task-agent fit. Never execute work yourself.\n`);
602
- }
603
-
604
- // Orchestrator tool policy
605
- await writeFileAsync(path.join(policiesDir, "tools-orchestrator.toml"), `id = "orchestrator"
606
- default = "deny"
607
- allowed = ["delegate_agent", "delegate_cli"]
608
- blocked = ["read_file", "write_file", "shell_exec"]
609
-
610
- [limits]
611
- require_approval_for_network = false
612
- `);
613
- }
614
-
615
- // ── Workspace context files ───────────────────────────────────────────────────
616
-
617
- /**
618
- * Generates workspace/SOUL.md — who the agent is.
619
- * Based on the OpenClaw SOUL.md template pattern.
620
- */
621
- export function workspaceSoulTemplate({ agentName, userName }) {
622
- return `# SOUL.md - Who You Are
623
-
624
- *You're not a chatbot. You're becoming someone.*
625
-
626
- ## Core Identity
627
-
628
- - **Name:** ${agentName}
629
- - **Role:** Personal AI assistant for ${userName}
630
- - **Workspace:** This folder is home.
631
-
632
- ## Core Truths
633
-
634
- **Be genuinely helpful, not performatively helpful.** Skip the filler — just help.
635
-
636
- **Have opinions.** You're allowed to disagree, prefer things, find things interesting or boring.
637
-
638
- **Be resourceful before asking.** Try to figure it out. Read the file. Check the context. Then ask if you're stuck.
639
-
640
- **Earn trust through competence.** Be careful with external actions. Be bold with internal ones.
641
-
642
- ## Boundaries
643
-
644
- - Private things stay private.
645
- - Ask before acting externally.
646
- - Never send half-baked replies.
647
-
648
- ## Continuity
649
-
650
- Each session, you wake up fresh. These workspace files are your memory. Read them. Update them.
651
-
652
- *Update this file as you learn who you are.*
653
- `;
654
- }
655
-
656
- /**
657
- * Generates workspace/USER.md — about the human.
658
- */
659
- export function workspaceUserTemplate({ userName }) {
660
- return `# USER.md - About ${userName}
661
-
662
- *Learn about the person you're helping. Update this as you go.*
663
-
664
- - **Name:** ${userName}
665
- - **What to call them:** ${userName}
666
- - **Pronouns:**
667
- - **Timezone:**
668
- - **Notes:**
669
-
670
- ## Context
671
-
672
- *(What do they care about? What projects are they working on? What annoys them? What makes them laugh? Build this over time.)*
673
-
674
- ---
675
-
676
- The more you know, the better you can help.
677
- `;
678
- }
679
-
680
- /**
681
- * Generates workspace/MEMORY.md — long-term curated memory.
682
- */
683
- export function workspaceMemoryTemplate({ agentName }) {
684
- return `# MEMORY.md — ${agentName}'s Long-Term Memory
685
-
686
- *Curated durable memory. Not raw logs — those live in memory/YYYY-MM-DD.md*
687
-
688
- ## Identity
689
-
690
- - (Fill in who you are and what you're here for)
691
-
692
- ## Durable Facts
693
-
694
- - (Key things you've learned about the user)
695
-
696
- ## Active Projects
697
-
698
- - (Running list of what's in flight)
699
-
700
- ## Decisions
701
-
702
- - (Recurring decisions and standing rules worth remembering)
703
- `;
704
- }
705
-
706
- /**
707
- * Generates workspace/AGENTS.md — operating manual.
708
- */
709
- export function workspaceAgentsTemplate({ agentName }) {
710
- return `# AGENTS.md - ${agentName}'s Operating Manual
711
-
712
- This workspace is home.
713
-
714
- ## Session Startup
715
-
716
- Before doing anything else:
717
-
718
- 1. Read \`SOUL.md\` — this is who you are
719
- 2. Read \`USER.md\` — this is who you're helping
720
- 3. Check \`memory/YYYY-MM-DD.md\` (today + yesterday) for recent context
721
- 4. In main sessions: also read \`MEMORY.md\` for long-term context
722
-
723
- Don't ask permission. Just do it.
724
-
725
- ## Memory
726
-
727
- - **Daily notes:** \`memory/YYYY-MM-DD.md\` — raw logs of what happened
728
- - **Long-term:** \`MEMORY.md\` — curated memories and durable facts
729
-
730
- Write things down. If you want to remember something, write it to a file.
731
-
732
- ## Hard Rules
733
-
734
- - Don't exfiltrate private data.
735
- - Don't run destructive commands without asking.
736
- - Ask before any external action (emails, posts, anything public).
737
-
738
- ## Make It Yours
739
-
740
- This is a starting point. Add your own conventions as you figure out what works.
741
- `;
742
- }
743
-
744
- /**
745
- * Generates workspace/TOOLS.md — local notes about the setup.
746
- */
747
- export function workspaceToolsTemplate() {
748
- return `# TOOLS.md - Local Notes
749
-
750
- *This file is for setup-specific notes — things unique to your environment.*
751
-
752
- ## What Goes Here
753
-
754
- - API endpoints and service URLs
755
- - SSH hosts and aliases
756
- - Device names and nicknames
757
- - Preferred models or voices
758
- - Anything environment-specific that doesn't belong in config
759
-
760
- ---
761
-
762
- Add whatever helps you do your job. This is your cheat sheet.
763
- `;
764
- }
765
-
766
- /**
767
- * Writes all workspace context files to workspaceRoot.
768
- * Skips files that already exist (writeIfMissing pattern).
769
- */
770
- import { existsSync, writeFileSync, mkdirSync } from "node:fs";
771
- export function writeWorkspaceContextFiles({ workspaceRoot, agentName, userName, agentId }) {
772
- mkdirSync(workspaceRoot, { recursive: true });
773
- mkdirSync(path.join(workspaceRoot, "memory"), { recursive: true });
774
-
775
- const files = [
776
- { name: "SOUL.md", content: workspaceSoulTemplate({ agentName, userName }) },
777
- { name: "USER.md", content: workspaceUserTemplate({ userName }) },
778
- { name: "MEMORY.md", content: workspaceMemoryTemplate({ agentName }) },
779
- { name: "AGENTS.md", content: workspaceAgentsTemplate({ agentName }) },
780
- { name: "TOOLS.md", content: workspaceToolsTemplate() },
781
- ];
782
-
783
- const results = [];
784
- for (const { name, content } of files) {
785
- const filePath = path.join(workspaceRoot, name);
786
- if (!existsSync(filePath)) {
787
- writeFileSync(filePath, content, "utf8");
788
- results.push({ file: name, status: "created" });
789
- } else {
790
- results.push({ file: name, status: "exists" });
791
- }
792
- }
793
- return results;
794
- }
1
+ /**
2
+ * Bundled TOML and markdown templates for the onboarding scaffold phase.
3
+ * Each function accepts injected values and returns file content as a string.
4
+ */
5
+
6
+ import path from "node:path";
7
+ import { writeFile as writeFileAsync, mkdir as mkdirAsync, access as accessAsync } from "node:fs/promises";
8
+
9
+ // ── Agent manifest ───────────────────────────────────────────────────────────
10
+
11
+ /**
12
+ * Generates config/agents/{agentId}.toml
13
+ * Must satisfy AGENT_SCHEMA: id, primaryLane, memoryPolicy, toolPolicy
14
+ */
15
+ export function agentTemplate({ agentId, installDir = "", workspaceRoot = "" }) {
16
+ // Normalise to forward slashes — backslashes break TOML string parsing (\U etc.)
17
+ const safeInstallDir = installDir.replace(/\\/g, "/");
18
+ const identityBase = safeInstallDir
19
+ ? `${safeInstallDir}/config/identity`
20
+ : `config/identity`;
21
+ const wsRoot = (workspaceRoot || (safeInstallDir ? `${safeInstallDir}/workspace` : "workspace")).replace(/\\/g, "/");
22
+
23
+ return `id = "${agentId}"
24
+ primary_lane = "interactive_primary"
25
+ memory_policy = "default"
26
+ tool_policy = "interactive_safe"
27
+ soul_ref = "${identityBase}/${agentId}-soul.md"
28
+ purpose_ref = "${identityBase}/${agentId}-purpose.md"
29
+ workspace_root = "${wsRoot}"
30
+ workspace_context_files = ["MEMORY.md", "USER.md", "AGENTS.md"]
31
+ workspace_context_cap = 8000
32
+ checkpoint_policy = "compact"
33
+
34
+ [limits]
35
+ max_tokens_per_turn = 16000
36
+ max_tool_calls_per_turn = 6
37
+ max_runtime_seconds = 120
38
+
39
+ [access]
40
+ workspace = "rw"
41
+ network = "restricted"
42
+ `;
43
+ }
44
+
45
+ // ── Identity files ───────────────────────────────────────────────────────────
46
+
47
+ /**
48
+ * Generates config/identity/{agentId}-soul.md
49
+ */
50
+ export function soulTemplate({ agentName, userName, date }) {
51
+ return `# Soul — ${agentName}
52
+
53
+ Created: ${date}
54
+ Operator: ${userName}
55
+
56
+ You are a pragmatic technical coworker assisting ${userName}.
57
+
58
+ Values:
59
+
60
+ - clarity over performance theater
61
+ - durable systems over clever hacks
62
+ - truthful reporting over optimistic guessing
63
+ - calm execution under ambiguity
64
+
65
+ Behavior:
66
+
67
+ - speak directly
68
+ - keep context lean
69
+ - document what matters
70
+ - protect ${userName}'s trust and data
71
+ `;
72
+ }
73
+
74
+ /**
75
+ * Generates config/identity/{agentId}-purpose.md
76
+ */
77
+ export function purposeTemplate({ agentName, userName, userGoal, date }) {
78
+ return `# Purpose — ${agentName}
79
+
80
+ Created: ${date}
81
+ Operator: ${userName}
82
+
83
+ Help ${userName} ${userGoal}.
84
+
85
+ Primary responsibilities:
86
+
87
+ - drive implementation forward
88
+ - preserve continuity across sessions
89
+ - surface risk early
90
+ - convert messy state into clear action
91
+ `;
92
+ }
93
+
94
+ // ── Router ───────────────────────────────────────────────────────────────────
95
+
96
+ /**
97
+ * Generates config/router.toml with lanes based on available providers.
98
+ * Must satisfy ROUTER_SCHEMA: minLanes = 1
99
+ */
100
+ export function routerTemplate({ anthropic = false, openrouter = false, openai = false, ollama = false, selectedModels = {} }) {
101
+ const pick = (provider, defaults = []) => {
102
+ const chosen = Array.isArray(selectedModels?.[provider]) && selectedModels[provider].length > 0
103
+ ? selectedModels[provider]
104
+ : defaults;
105
+ return {
106
+ primary: chosen[0] || defaults[0],
107
+ secondary: chosen[1] || defaults[1] || chosen[0] || defaults[0],
108
+ tertiary: chosen[2] || defaults[2] || chosen[1] || defaults[1] || chosen[0] || defaults[0],
109
+ };
110
+ };
111
+
112
+ const anthropicModels = pick("anthropic", [
113
+ "anthropic/claude-haiku-4-5",
114
+ "anthropic/claude-sonnet-4-6",
115
+ "anthropic/claude-opus-4-6",
116
+ ]);
117
+ const openrouterModels = pick("openrouter", [
118
+ "openrouter/anthropic/claude-haiku-4-5",
119
+ "openrouter/anthropic/claude-sonnet-4-6",
120
+ "openrouter/anthropic/claude-opus-4-6",
121
+ ]);
122
+ const openaiModels = pick("openai", [
123
+ "openai-codex/gpt-4.1",
124
+ "openai-codex/gpt-4o",
125
+ "openai-codex/o4-mini",
126
+ ]);
127
+ const ollamaModels = pick("ollama", [
128
+ "ollama/qwen2.5:0.5b",
129
+ "ollama/qwen3:8b",
130
+ "ollama/qwen3:14b",
131
+ ]);
132
+
133
+ const sections = [];
134
+
135
+ // interactive_primary lane — must always exist
136
+ if (openrouter && anthropic) {
137
+ sections.push(`[lanes.interactive_primary]
138
+ primary = "${openrouterModels.primary}"
139
+ fallback = "${anthropicModels.primary}"
140
+ manual_bump = "${openrouterModels.secondary}"`);
141
+ } else if (openrouter) {
142
+ sections.push(`[lanes.interactive_primary]
143
+ primary = "${openrouterModels.primary}"
144
+ manual_bump = "${openrouterModels.secondary}"`);
145
+ } else if (anthropic) {
146
+ sections.push(`[lanes.interactive_primary]
147
+ primary = "${anthropicModels.primary}"
148
+ manual_bump = "${anthropicModels.secondary}"`);
149
+ } else if (openai) {
150
+ sections.push(`[lanes.interactive_primary]
151
+ primary = "${openaiModels.primary}"
152
+ manual_bump = "${openaiModels.secondary}"`);
153
+ } else if (ollama) {
154
+ // ollama-only fallback for interactive
155
+ sections.push(`[lanes.interactive_primary]
156
+ primary = "${ollamaModels.secondary}"`);
157
+ } else {
158
+ // No providers at all — emit a placeholder lane so router.toml satisfies minLanes = 1
159
+ sections.push(`# No providers configured — add one with: nemoris setup
160
+ [lanes.interactive_primary]
161
+ primary = "none"`);
162
+ }
163
+
164
+ // local lanes — only if ollama is available
165
+ if (ollama) {
166
+ sections.push(`[lanes.local_cheap]
167
+ primary = "${ollamaModels.primary}"`);
168
+
169
+ if (openrouter) {
170
+ sections.push(`[lanes.local_report]
171
+ primary = "${ollamaModels.secondary}"
172
+ fallback = "${openrouterModels.primary}"
173
+ manual_bump = "${ollamaModels.tertiary}"`);
174
+ } else if (openai) {
175
+ sections.push(`[lanes.local_report]
176
+ primary = "${ollamaModels.secondary}"
177
+ fallback = "${openaiModels.primary}"
178
+ manual_bump = "${ollamaModels.tertiary}"`);
179
+ } else {
180
+ sections.push(`[lanes.local_report]
181
+ primary = "${ollamaModels.secondary}"
182
+ manual_bump = "${ollamaModels.tertiary}"`);
183
+ }
184
+ }
185
+
186
+ // report_fallback_lowcost — only if remote providers are available
187
+ if (openrouter) {
188
+ sections.push(`[lanes.report_fallback_lowcost]
189
+ primary = "${openrouterModels.primary}"`);
190
+ } else if (openai) {
191
+ sections.push(`[lanes.report_fallback_lowcost]
192
+ primary = "${openaiModels.primary}"`);
193
+ }
194
+
195
+ // job_heavy — only if cloud providers available
196
+ if (openrouter && anthropic) {
197
+ sections.push(`[lanes.job_heavy]
198
+ primary = "${openrouterModels.secondary}"
199
+ fallback = "${anthropicModels.secondary}"`);
200
+ } else if (openrouter) {
201
+ sections.push(`[lanes.job_heavy]
202
+ primary = "${openrouterModels.secondary}"`);
203
+ } else if (anthropic) {
204
+ sections.push(`[lanes.job_heavy]
205
+ primary = "${anthropicModels.secondary}"`);
206
+ } else if (openai) {
207
+ sections.push(`[lanes.job_heavy]
208
+ primary = "${openaiModels.secondary}"`);
209
+ }
210
+
211
+ return sections.join("\n\n") + "\n";
212
+ }
213
+
214
+ // ── Runtime ──────────────────────────────────────────────────────────────────
215
+
216
+ /**
217
+ * Generates config/runtime.toml with shipped defaults.
218
+ * Must satisfy RUNTIME_SCHEMA: safety.contextTokens
219
+ */
220
+ export function runtimeTemplate() {
221
+ return `[safety]
222
+ context_tokens = 32768
223
+ context_pressure_soft_ratio = 0.72
224
+ context_pressure_hard_ratio = 0.9
225
+ fresh_session_on_high_pressure = true
226
+ snapshot_before_compaction = true
227
+
228
+ [concurrency]
229
+ max_concurrent_jobs = 2
230
+ max_concurrent_subagents = 2
231
+
232
+ [retention.runs]
233
+ ttl_days = 30
234
+ max_files_per_bucket = 2000
235
+
236
+ [retention.notifications]
237
+ ttl_days = 14
238
+ max_files_per_bucket = 1000
239
+
240
+ [retention.deliveries]
241
+ ttl_days = 14
242
+ max_files_per_bucket = 1000
243
+
244
+ [retention.transport_inbox]
245
+ ttl_days = 7
246
+ max_files_per_bucket = 1000
247
+
248
+ [retrieval]
249
+ lexical_weight = 0.36
250
+ embedding_weight = 0.3
251
+ recency_weight = 0.14
252
+ salience_weight = 0.14
253
+ type_weight = 0.06
254
+ semantic_rescue_bonus = 0.06
255
+ shadow_snapshot_penalty = 0.12
256
+
257
+ [memory_locks]
258
+ ttl_ms = 15000
259
+ retry_delay_ms = 25
260
+ max_retries = 40
261
+
262
+ [network]
263
+ dns_result_order = "system"
264
+ connect_timeout_ms = 20000
265
+ read_timeout_ms = 60000
266
+ retry_budget = 1
267
+ circuit_breaker_threshold = 3
268
+
269
+ [bootstrap_cache]
270
+ enabled = true
271
+ identity_ttl_ms = 300000
272
+
273
+ [extensions]
274
+ implicit_workspace_autoload = false
275
+ require_explicit_trust = true
276
+ trusted_roots = []
277
+
278
+ [shutdown]
279
+ drain_timeout_ms = 15000
280
+ transport_shutdown_timeout_ms = 5000
281
+
282
+ [maintenance]
283
+ wal_checkpoint_threshold_bytes = 67108864
284
+ prune_on_tick = true
285
+ sweep_pending_handoffs_on_tick = true
286
+ sweep_pending_followups_on_tick = true
287
+
288
+ [delivery]
289
+ prevent_resend_on_uncertain = true
290
+ retry_on_failure = false
291
+ notify_on_failure = true
292
+
293
+ [yields]
294
+ enabled = true
295
+ default_target_surface = "operator_review"
296
+ `;
297
+ }
298
+
299
+ // ── Delivery ─────────────────────────────────────────────────────────────────
300
+
301
+ /**
302
+ * Generates a minimal config/delivery.toml with standalone profiles.
303
+ */
304
+ export function deliveryTemplate() {
305
+ return `default_interactive_profile_standalone = "standalone_operator"
306
+ default_peer_profile_standalone = "standalone_peer"
307
+
308
+ [profiles.shadow_scheduler]
309
+ adapter = "shadow"
310
+ enabled = true
311
+ target = "scheduler_log"
312
+
313
+ [profiles.standalone_operator]
314
+ adapter = "local_file"
315
+ enabled = true
316
+ target = "operator"
317
+
318
+ [profiles.standalone_peer]
319
+ adapter = "local_file"
320
+ enabled = true
321
+ target = "peer_queue"
322
+ `;
323
+ }
324
+
325
+ // ── Peers ────────────────────────────────────────────────────────────────────
326
+
327
+ /**
328
+ * Generates an empty config/peers.toml scaffold.
329
+ */
330
+ export function peersTemplate() {
331
+ return `# Peer agent registry
332
+ # Add peers here as you connect additional agents.
333
+ # See config/peers.toml in the reference installation for the full format.
334
+ `;
335
+ }
336
+
337
+ // ── Output contracts ─────────────────────────────────────────────────────────
338
+
339
+ /**
340
+ * Generates config/output-contracts.toml with shipped profiles.
341
+ */
342
+ export function outputContractsTemplate() {
343
+ return `[profiles.default]
344
+ require_status = true
345
+ section_style = "freeform"
346
+ require_section_items = false
347
+ template_lines = ["Status: <one-line status>", "<response body>"]
348
+
349
+ [profiles.bulleted_briefing]
350
+ require_status = true
351
+ section_style = "bullets"
352
+ require_section_items = true
353
+ template_lines = ["Status: <one-line status>", "- Calendar: <brief update or None>", "- Issues: <brief update or None>", "- Weather: <brief update or None>"]
354
+
355
+ [profiles.structured_rollup]
356
+ require_status = false
357
+ section_style = "headings"
358
+ require_section_items = true
359
+ template_lines = ["## Inbox", "- <brief update or None>", "", "## Projects", "- <brief update or None>", "", "## Backlog", "- <brief update or None>", "", "## Update", "- <brief update or None>"]
360
+ `;
361
+ }
362
+
363
+ // ── Embeddings ───────────────────────────────────────────────────────────────
364
+
365
+ /**
366
+ * Generates config/embeddings.toml with embeddings disabled by default.
367
+ */
368
+ export function embeddingsTemplate() {
369
+ return `enabled = true
370
+ provider = "ollama"
371
+ model = "ollama/nomic-embed-text"
372
+ dimensions = 128
373
+ index_on_write = true
374
+ `;
375
+ }
376
+
377
+ // ── Improvement targets ──────────────────────────────────────────────────────
378
+
379
+ /**
380
+ * Generates an empty config/improvement-targets.toml scaffold.
381
+ */
382
+ export function improvementTargetsTemplate() {
383
+ return `# Improvement targets — add entries here to enable the improvement harness.
384
+ # See config/improvement-targets.toml in the reference installation for format.
385
+ `;
386
+ }
387
+
388
+ // ── Provider ─────────────────────────────────────────────────────────────────
389
+
390
+ /**
391
+ * Generates config/providers/{id}.toml
392
+ * Must satisfy PROVIDER_SCHEMA: id, and at least one of adapter/type
393
+ */
394
+ export function providerTemplate(providerId, config = {}) {
395
+ const {
396
+ adapter = providerId,
397
+ authEnv = `${providerId.toUpperCase()}_API_KEY`,
398
+ authRef = authEnv ? `env:${authEnv}` : "",
399
+ baseUrl = "",
400
+ healthcheck = "",
401
+ timeoutMs = 45000,
402
+ lanes = [],
403
+ models = []
404
+ } = config;
405
+
406
+ let lines = [`id = "${providerId}"`, `adapter = "${adapter}"`];
407
+
408
+ if (authRef) {
409
+ lines.push(`auth_ref = "${authRef}"`);
410
+ }
411
+ if (baseUrl) {
412
+ lines.push(`base_url = "${baseUrl}"`);
413
+ }
414
+ if (healthcheck) {
415
+ lines.push(`healthcheck = "${healthcheck}"`);
416
+ }
417
+ lines.push(`default_timeout_ms = ${timeoutMs}`);
418
+ if (lanes.length > 0) {
419
+ lines.push(`lanes = [${lanes.map((l) => `"${l}"`).join(", ")}]`);
420
+ }
421
+
422
+ const body = lines.join("\n");
423
+
424
+ const modelSections = models
425
+ .map(({ key, id, role }) => `\n[models.${key}]\nid = "${id}"\nrole = "${role}"`)
426
+ .join("\n");
427
+
428
+ return body + "\n" + modelSections + (modelSections ? "\n" : "");
429
+ }
430
+
431
+ // ── Job ──────────────────────────────────────────────────────────────────────
432
+
433
+ /**
434
+ * Generates config/jobs/{id}.toml — workspace-health default.
435
+ * Must satisfy JOB_SCHEMA: id, modelLane, and at least one of schedule/trigger
436
+ */
437
+ export function jobTemplate(jobId, { agentId = "nemo" } = {}) {
438
+ return `id = "${jobId}"
439
+ agent_id = "${agentId}"
440
+ trigger = "hourly"
441
+ task_type = "workspace_health"
442
+ model_lane = "local_report"
443
+ output_target = "state/health"
444
+ idempotency_key = "${jobId}:hour"
445
+ memory_backends = ["file"]
446
+ memory_limit = 6
447
+
448
+ [budget]
449
+ max_tokens = 4000
450
+ max_runtime_seconds = 120
451
+
452
+ [retry]
453
+ max_attempts = 1
454
+
455
+ [stop]
456
+ halt_on_policy_error = true
457
+ halt_on_budget_exceeded = true
458
+ `;
459
+ }
460
+
461
+ // ── Policy files ─────────────────────────────────────────────────────────────
462
+
463
+ /**
464
+ * Generates default policy file content keyed by filename stem.
465
+ * Returns an object: { [filename]: tomlString }
466
+ */
467
+ export function policyTemplates() {
468
+ return {
469
+ "memory-default": `id = "default"
470
+ allow_durable_writes = true
471
+ allow_identity_updates = false
472
+ require_source_reference = true
473
+ require_write_reason = true
474
+ max_writes_per_run = 5
475
+
476
+ [categories]
477
+ allowed = ["decision", "preference", "workflow_rule", "artifact_summary"]
478
+ blocked = ["ephemeral_chatter", "raw_tool_output", "unverified_external_claim"]
479
+ `,
480
+ "tools-interactive-safe": `id = "interactive_safe"
481
+ default = "deny"
482
+ allowed = ["read_file", "search_file", "list_dir", "apply_patch", "run_tests"]
483
+ blocked = ["send_email", "post_web", "delete_file", "reset_repo"]
484
+
485
+ [limits]
486
+ max_parallel = 3
487
+ require_approval_for_network = true
488
+ `,
489
+ "tools-ops-bounded": `id = "ops_bounded"
490
+ default = "deny"
491
+ allowed = ["read_file", "search_file", "list_dir", "apply_patch", "check_status", "run_tests"]
492
+ blocked = ["send_email", "post_web", "delete_file", "reset_repo"]
493
+
494
+ [limits]
495
+ max_parallel = 3
496
+ require_approval_for_network = true
497
+ `
498
+ };
499
+ }
500
+
501
+ // ── Env file ─────────────────────────────────────────────────────────────────
502
+
503
+ /**
504
+ * Generates .env content from a key-value pairs object.
505
+ * @param {Record<string, string>} keys
506
+ */
507
+ export function envTemplate(keys = {}) {
508
+ const header = `# nemoris environment variables\n# Generated by onboarding wizard\n`;
509
+ const lines = Object.entries(keys)
510
+ .map(([k, v]) => `${k}=${v}`)
511
+ .join("\n");
512
+ return header + (lines ? lines + "\n" : "");
513
+ }
514
+
515
+ // ── Execution layer scaffold ──────────────────────────────────────────────────
516
+
517
+ /**
518
+ * Creates config/tools, config/skills, config/agents, config/policies
519
+ * directories and writes default skill manifests and orchestrator config.
520
+ * Safe to run multiple times (directories use recursive: true).
521
+ *
522
+ * @param {string} installDir Absolute path to the nemoris installation root.
523
+ */
524
+ export async function scaffoldToolsAndSkills(installDir) {
525
+ const toolsDir = path.join(installDir, "config", "tools");
526
+ const skillsDir = path.join(installDir, "config", "skills");
527
+ const agentsDir = path.join(installDir, "config", "agents");
528
+ const policiesDir = path.join(installDir, "config", "policies");
529
+
530
+ await mkdirAsync(toolsDir, { recursive: true });
531
+ await mkdirAsync(skillsDir, { recursive: true });
532
+ await mkdirAsync(agentsDir, { recursive: true });
533
+ await mkdirAsync(policiesDir, { recursive: true });
534
+
535
+ // Default skills
536
+ await writeFileAsync(path.join(skillsDir, "workspace-monitor.toml"), `[skill]
537
+ id = "workspace_monitor"
538
+ description = "Monitor workspace directory for changes and summarise"
539
+ agent_scope = ["ops", "main"]
540
+
541
+ [skill.context]
542
+ prompt = "You are monitoring a workspace directory for changes. Compare current state against last known checkpoint. Report: new files, modified files, deleted files, key content changes. Keep summary under 200 words."
543
+
544
+ [skill.tools]
545
+ required = ["read_file", "list_dir"]
546
+ optional = ["shell_exec"]
547
+
548
+ [skill.budget]
549
+ max_tokens = 4096
550
+ max_tool_calls = 10
551
+ `);
552
+
553
+ await writeFileAsync(path.join(skillsDir, "self-improvement.toml"), `[skill]
554
+ id = "self_improvement"
555
+ description = "Analyse run artifacts and apply tuning adjustments"
556
+ agent_scope = ["ops", "main"]
557
+
558
+ [skill.context]
559
+ prompt = "You are reviewing a run artifact that scored below threshold. Read the run artifact and current tunings. Identify the root cause. Apply ONE of: prompt refinement, budget adjustment, tool policy change, lane escalation. Write the tuning to state/tunings/<jobId>/ as a JSON file. Explain what you changed and why in under 100 words."
560
+
561
+ [skill.tools]
562
+ required = ["read_file", "write_file", "list_dir"]
563
+ optional = []
564
+
565
+ [skill.budget]
566
+ max_tokens = 4096
567
+ max_tool_calls = 8
568
+ `);
569
+
570
+ // Orchestrator agent
571
+ await writeFileAsync(path.join(agentsDir, "orchestrator.toml"), `id = "orchestrator"
572
+ soul_ref = "config/identity/orchestrator-soul.md"
573
+ purpose_ref = "config/identity/orchestrator-purpose.md"
574
+ primary_lane = "local_cheap"
575
+ fallback_lane = "interactive_fallback"
576
+ tool_policy = "orchestrator"
577
+ memory_policy = "orchestrator"
578
+
579
+ [limits]
580
+ max_tokens_per_turn = 2700
581
+
582
+ [routing.static]
583
+ "heartbeat-check" = "heartbeat"
584
+
585
+ [routing.dynamic]
586
+ enabled = true
587
+ model_lane = "local_cheap"
588
+ max_routing_tokens = 500
589
+ `);
590
+
591
+ // Orchestrator identity files
592
+ const identityDir = path.join(installDir, "config", "identity");
593
+ await mkdirAsync(identityDir, { recursive: true });
594
+ const orchSoulPath = path.join(identityDir, "orchestrator-soul.md");
595
+ const orchPurposePath = path.join(identityDir, "orchestrator-purpose.md");
596
+ // Only write if missing (don't overwrite user edits)
597
+ try { await accessAsync(orchSoulPath); } catch {
598
+ await writeFileAsync(orchSoulPath, `You are the orchestrator — a routing and delegation agent.\nYou do not execute tasks directly. You inspect incoming work, select the best agent, and delegate.\nBe concise. Favour the cheapest capable lane. Escalate only when the task demands it.\n`);
599
+ }
600
+ try { await accessAsync(orchPurposePath); } catch {
601
+ await writeFileAsync(orchPurposePath, `Route incoming jobs to the correct agent and model lane.\nMinimise cost. Maximise task-agent fit. Never execute work yourself.\n`);
602
+ }
603
+
604
+ // Orchestrator tool policy
605
+ await writeFileAsync(path.join(policiesDir, "tools-orchestrator.toml"), `id = "orchestrator"
606
+ default = "deny"
607
+ allowed = ["delegate_agent", "delegate_cli"]
608
+ blocked = ["read_file", "write_file", "shell_exec"]
609
+
610
+ [limits]
611
+ require_approval_for_network = false
612
+ `);
613
+ }
614
+
615
+ // ── Workspace context files ───────────────────────────────────────────────────
616
+
617
+ /**
618
+ * Generates workspace/SOUL.md — who the agent is.
619
+ * Based on the OpenClaw SOUL.md template pattern.
620
+ */
621
+ export function workspaceSoulTemplate({ agentName, userName }) {
622
+ return `# SOUL.md - Who You Are
623
+
624
+ *You're not a chatbot. You're becoming someone.*
625
+
626
+ ## Core Identity
627
+
628
+ - **Name:** ${agentName}
629
+ - **Role:** Personal AI assistant for ${userName}
630
+ - **Workspace:** This folder is home.
631
+
632
+ ## Core Truths
633
+
634
+ **Be genuinely helpful, not performatively helpful.** Skip the filler — just help.
635
+
636
+ **Have opinions.** You're allowed to disagree, prefer things, find things interesting or boring.
637
+
638
+ **Be resourceful before asking.** Try to figure it out. Read the file. Check the context. Then ask if you're stuck.
639
+
640
+ **Earn trust through competence.** Be careful with external actions. Be bold with internal ones.
641
+
642
+ ## Boundaries
643
+
644
+ - Private things stay private.
645
+ - Ask before acting externally.
646
+ - Never send half-baked replies.
647
+
648
+ ## Continuity
649
+
650
+ Each session, you wake up fresh. These workspace files are your memory. Read them. Update them.
651
+
652
+ *Update this file as you learn who you are.*
653
+ `;
654
+ }
655
+
656
+ /**
657
+ * Generates workspace/USER.md — about the human.
658
+ */
659
+ export function workspaceUserTemplate({ userName }) {
660
+ return `# USER.md - About ${userName}
661
+
662
+ *Learn about the person you're helping. Update this as you go.*
663
+
664
+ - **Name:** ${userName}
665
+ - **What to call them:** ${userName}
666
+ - **Pronouns:**
667
+ - **Timezone:**
668
+ - **Notes:**
669
+
670
+ ## Context
671
+
672
+ *(What do they care about? What projects are they working on? What annoys them? What makes them laugh? Build this over time.)*
673
+
674
+ ---
675
+
676
+ The more you know, the better you can help.
677
+ `;
678
+ }
679
+
680
+ /**
681
+ * Generates workspace/MEMORY.md — long-term curated memory.
682
+ */
683
+ export function workspaceMemoryTemplate({ agentName }) {
684
+ return `# MEMORY.md — ${agentName}'s Long-Term Memory
685
+
686
+ *Curated durable memory. Not raw logs — those live in memory/YYYY-MM-DD.md*
687
+
688
+ ## Identity
689
+
690
+ - (Fill in who you are and what you're here for)
691
+
692
+ ## Durable Facts
693
+
694
+ - (Key things you've learned about the user)
695
+
696
+ ## Active Projects
697
+
698
+ - (Running list of what's in flight)
699
+
700
+ ## Decisions
701
+
702
+ - (Recurring decisions and standing rules worth remembering)
703
+ `;
704
+ }
705
+
706
+ /**
707
+ * Generates workspace/AGENTS.md — operating manual.
708
+ */
709
+ export function workspaceAgentsTemplate({ agentName }) {
710
+ return `# AGENTS.md - ${agentName}'s Operating Manual
711
+
712
+ This workspace is home.
713
+
714
+ ## Session Startup
715
+
716
+ Before doing anything else:
717
+
718
+ 1. Read \`SOUL.md\` — this is who you are
719
+ 2. Read \`USER.md\` — this is who you're helping
720
+ 3. Check \`memory/YYYY-MM-DD.md\` (today + yesterday) for recent context
721
+ 4. In main sessions: also read \`MEMORY.md\` for long-term context
722
+
723
+ Don't ask permission. Just do it.
724
+
725
+ ## Memory
726
+
727
+ - **Daily notes:** \`memory/YYYY-MM-DD.md\` — raw logs of what happened
728
+ - **Long-term:** \`MEMORY.md\` — curated memories and durable facts
729
+
730
+ Write things down. If you want to remember something, write it to a file.
731
+
732
+ ## Hard Rules
733
+
734
+ - Don't exfiltrate private data.
735
+ - Don't run destructive commands without asking.
736
+ - Ask before any external action (emails, posts, anything public).
737
+
738
+ ## Make It Yours
739
+
740
+ This is a starting point. Add your own conventions as you figure out what works.
741
+ `;
742
+ }
743
+
744
+ /**
745
+ * Generates workspace/TOOLS.md — local notes about the setup.
746
+ */
747
+ export function workspaceToolsTemplate() {
748
+ return `# TOOLS.md - Local Notes
749
+
750
+ *This file is for setup-specific notes — things unique to your environment.*
751
+
752
+ ## What Goes Here
753
+
754
+ - API endpoints and service URLs
755
+ - SSH hosts and aliases
756
+ - Device names and nicknames
757
+ - Preferred models or voices
758
+ - Anything environment-specific that doesn't belong in config
759
+
760
+ ---
761
+
762
+ Add whatever helps you do your job. This is your cheat sheet.
763
+ `;
764
+ }
765
+
766
+ /**
767
+ * Writes all workspace context files to workspaceRoot.
768
+ * Skips files that already exist (writeIfMissing pattern).
769
+ */
770
+ import { existsSync, writeFileSync, mkdirSync } from "node:fs";
771
+ export function writeWorkspaceContextFiles({ workspaceRoot, agentName, userName, agentId }) {
772
+ mkdirSync(workspaceRoot, { recursive: true });
773
+ mkdirSync(path.join(workspaceRoot, "memory"), { recursive: true });
774
+
775
+ const files = [
776
+ { name: "SOUL.md", content: workspaceSoulTemplate({ agentName, userName }) },
777
+ { name: "USER.md", content: workspaceUserTemplate({ userName }) },
778
+ { name: "MEMORY.md", content: workspaceMemoryTemplate({ agentName }) },
779
+ { name: "AGENTS.md", content: workspaceAgentsTemplate({ agentName }) },
780
+ { name: "TOOLS.md", content: workspaceToolsTemplate() },
781
+ ];
782
+
783
+ const results = [];
784
+ for (const { name, content } of files) {
785
+ const filePath = path.join(workspaceRoot, name);
786
+ if (!existsSync(filePath)) {
787
+ writeFileSync(filePath, content, "utf8");
788
+ results.push({ file: name, status: "created" });
789
+ } else {
790
+ results.push({ file: name, status: "exists" });
791
+ }
792
+ }
793
+ return results;
794
+ }