nemoris 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/.env.example +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +209 -0
  4. package/SECURITY.md +119 -0
  5. package/bin/nemoris +46 -0
  6. package/config/agents/agent.toml.example +28 -0
  7. package/config/agents/default.toml +22 -0
  8. package/config/agents/orchestrator.toml +18 -0
  9. package/config/delivery.toml +73 -0
  10. package/config/embeddings.toml +5 -0
  11. package/config/identity/default-purpose.md +1 -0
  12. package/config/identity/default-soul.md +3 -0
  13. package/config/identity/orchestrator-purpose.md +1 -0
  14. package/config/identity/orchestrator-soul.md +1 -0
  15. package/config/improvement-targets.toml +15 -0
  16. package/config/jobs/heartbeat-check.toml +30 -0
  17. package/config/jobs/memory-rollup.toml +46 -0
  18. package/config/jobs/workspace-health.toml +63 -0
  19. package/config/mcp.toml +16 -0
  20. package/config/output-contracts.toml +17 -0
  21. package/config/peers.toml +32 -0
  22. package/config/peers.toml.example +32 -0
  23. package/config/policies/memory-default.toml +10 -0
  24. package/config/policies/memory-heartbeat.toml +5 -0
  25. package/config/policies/memory-ops.toml +10 -0
  26. package/config/policies/tools-heartbeat-minimal.toml +8 -0
  27. package/config/policies/tools-interactive-safe.toml +8 -0
  28. package/config/policies/tools-ops-bounded.toml +8 -0
  29. package/config/policies/tools-orchestrator.toml +7 -0
  30. package/config/providers/anthropic.toml +15 -0
  31. package/config/providers/ollama.toml +5 -0
  32. package/config/providers/openai-codex.toml +9 -0
  33. package/config/providers/openrouter.toml +5 -0
  34. package/config/router.toml +22 -0
  35. package/config/runtime.toml +114 -0
  36. package/config/skills/self-improvement.toml +15 -0
  37. package/config/skills/telegram-onboarding-spec.md +240 -0
  38. package/config/skills/workspace-monitor.toml +15 -0
  39. package/config/task-router.toml +42 -0
  40. package/install.sh +50 -0
  41. package/package.json +90 -0
  42. package/src/auth/auth-profiles.js +169 -0
  43. package/src/auth/openai-codex-oauth.js +285 -0
  44. package/src/battle.js +449 -0
  45. package/src/cli/help.js +265 -0
  46. package/src/cli/output-filter.js +49 -0
  47. package/src/cli/runtime-control.js +704 -0
  48. package/src/cli-main.js +2763 -0
  49. package/src/cli.js +78 -0
  50. package/src/config/loader.js +332 -0
  51. package/src/config/schema-validator.js +214 -0
  52. package/src/config/toml-lite.js +8 -0
  53. package/src/daemon/action-handlers.js +71 -0
  54. package/src/daemon/healing-tick.js +87 -0
  55. package/src/daemon/health-probes.js +90 -0
  56. package/src/daemon/notifier.js +57 -0
  57. package/src/daemon/nurse.js +218 -0
  58. package/src/daemon/repair-log.js +106 -0
  59. package/src/daemon/rule-staging.js +90 -0
  60. package/src/daemon/rules.js +29 -0
  61. package/src/daemon/telegram-commands.js +54 -0
  62. package/src/daemon/updater.js +85 -0
  63. package/src/jobs/job-runner.js +78 -0
  64. package/src/mcp/consumer.js +129 -0
  65. package/src/memory/active-recall.js +171 -0
  66. package/src/memory/backend-manager.js +97 -0
  67. package/src/memory/backends/file-backend.js +38 -0
  68. package/src/memory/backends/qmd-backend.js +219 -0
  69. package/src/memory/embedding-guards.js +24 -0
  70. package/src/memory/embedding-index.js +118 -0
  71. package/src/memory/embedding-service.js +179 -0
  72. package/src/memory/file-index.js +177 -0
  73. package/src/memory/memory-signature.js +5 -0
  74. package/src/memory/memory-store.js +648 -0
  75. package/src/memory/retrieval-planner.js +66 -0
  76. package/src/memory/scoring.js +145 -0
  77. package/src/memory/simhash.js +78 -0
  78. package/src/memory/sqlite-active-store.js +824 -0
  79. package/src/memory/write-policy.js +36 -0
  80. package/src/onboarding/aliases.js +33 -0
  81. package/src/onboarding/auth/api-key.js +224 -0
  82. package/src/onboarding/auth/ollama-detect.js +42 -0
  83. package/src/onboarding/clack-prompter.js +77 -0
  84. package/src/onboarding/doctor.js +530 -0
  85. package/src/onboarding/lock.js +42 -0
  86. package/src/onboarding/model-catalog.js +344 -0
  87. package/src/onboarding/phases/auth.js +589 -0
  88. package/src/onboarding/phases/build.js +130 -0
  89. package/src/onboarding/phases/choose.js +82 -0
  90. package/src/onboarding/phases/detect.js +98 -0
  91. package/src/onboarding/phases/hatch.js +216 -0
  92. package/src/onboarding/phases/identity.js +79 -0
  93. package/src/onboarding/phases/ollama.js +345 -0
  94. package/src/onboarding/phases/scaffold.js +99 -0
  95. package/src/onboarding/phases/telegram.js +377 -0
  96. package/src/onboarding/phases/validate.js +204 -0
  97. package/src/onboarding/phases/verify.js +206 -0
  98. package/src/onboarding/platform.js +482 -0
  99. package/src/onboarding/status-bar.js +95 -0
  100. package/src/onboarding/templates.js +794 -0
  101. package/src/onboarding/toml-writer.js +38 -0
  102. package/src/onboarding/tui.js +250 -0
  103. package/src/onboarding/uninstall.js +153 -0
  104. package/src/onboarding/wizard.js +499 -0
  105. package/src/providers/anthropic.js +168 -0
  106. package/src/providers/base.js +247 -0
  107. package/src/providers/circuit-breaker.js +136 -0
  108. package/src/providers/ollama.js +163 -0
  109. package/src/providers/openai-codex.js +149 -0
  110. package/src/providers/openrouter.js +136 -0
  111. package/src/providers/registry.js +36 -0
  112. package/src/providers/router.js +16 -0
  113. package/src/runtime/bootstrap-cache.js +47 -0
  114. package/src/runtime/capabilities-prompt.js +25 -0
  115. package/src/runtime/completion-ping.js +99 -0
  116. package/src/runtime/config-validator.js +121 -0
  117. package/src/runtime/context-ledger.js +360 -0
  118. package/src/runtime/cutover-readiness.js +42 -0
  119. package/src/runtime/daemon.js +729 -0
  120. package/src/runtime/delivery-ack.js +195 -0
  121. package/src/runtime/delivery-adapters/local-file.js +41 -0
  122. package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
  123. package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
  124. package/src/runtime/delivery-adapters/shadow.js +13 -0
  125. package/src/runtime/delivery-adapters/standalone-http.js +98 -0
  126. package/src/runtime/delivery-adapters/telegram.js +104 -0
  127. package/src/runtime/delivery-adapters/tui.js +128 -0
  128. package/src/runtime/delivery-manager.js +807 -0
  129. package/src/runtime/delivery-store.js +168 -0
  130. package/src/runtime/dependency-health.js +118 -0
  131. package/src/runtime/envelope.js +114 -0
  132. package/src/runtime/evaluation.js +1089 -0
  133. package/src/runtime/exec-approvals.js +216 -0
  134. package/src/runtime/executor.js +500 -0
  135. package/src/runtime/failure-ping.js +67 -0
  136. package/src/runtime/flows.js +83 -0
  137. package/src/runtime/guards.js +45 -0
  138. package/src/runtime/handoff.js +51 -0
  139. package/src/runtime/identity-cache.js +28 -0
  140. package/src/runtime/improvement-engine.js +109 -0
  141. package/src/runtime/improvement-harness.js +581 -0
  142. package/src/runtime/input-sanitiser.js +72 -0
  143. package/src/runtime/interaction-contract.js +347 -0
  144. package/src/runtime/lane-readiness.js +226 -0
  145. package/src/runtime/migration.js +323 -0
  146. package/src/runtime/model-resolution.js +78 -0
  147. package/src/runtime/network.js +64 -0
  148. package/src/runtime/notification-store.js +97 -0
  149. package/src/runtime/notifier.js +256 -0
  150. package/src/runtime/orchestrator.js +53 -0
  151. package/src/runtime/orphan-reaper.js +41 -0
  152. package/src/runtime/output-contract-schema.js +139 -0
  153. package/src/runtime/output-contract-validator.js +439 -0
  154. package/src/runtime/peer-readiness.js +69 -0
  155. package/src/runtime/peer-registry.js +133 -0
  156. package/src/runtime/pilot-status.js +108 -0
  157. package/src/runtime/prompt-builder.js +261 -0
  158. package/src/runtime/provider-attempt.js +582 -0
  159. package/src/runtime/report-fallback.js +71 -0
  160. package/src/runtime/result-normalizer.js +183 -0
  161. package/src/runtime/retention.js +74 -0
  162. package/src/runtime/review.js +244 -0
  163. package/src/runtime/route-job.js +15 -0
  164. package/src/runtime/run-store.js +38 -0
  165. package/src/runtime/schedule.js +88 -0
  166. package/src/runtime/scheduler-state.js +434 -0
  167. package/src/runtime/scheduler.js +656 -0
  168. package/src/runtime/session-compactor.js +182 -0
  169. package/src/runtime/session-search.js +155 -0
  170. package/src/runtime/slack-inbound.js +249 -0
  171. package/src/runtime/ssrf.js +102 -0
  172. package/src/runtime/status-aggregator.js +330 -0
  173. package/src/runtime/task-contract.js +140 -0
  174. package/src/runtime/task-packet.js +107 -0
  175. package/src/runtime/task-router.js +140 -0
  176. package/src/runtime/telegram-inbound.js +1565 -0
  177. package/src/runtime/token-counter.js +134 -0
  178. package/src/runtime/token-estimator.js +59 -0
  179. package/src/runtime/tool-loop.js +200 -0
  180. package/src/runtime/transport-server.js +311 -0
  181. package/src/runtime/tui-server.js +411 -0
  182. package/src/runtime/ulid.js +44 -0
  183. package/src/security/ssrf-check.js +197 -0
  184. package/src/setup.js +369 -0
  185. package/src/shadow/bridge.js +303 -0
  186. package/src/skills/loader.js +84 -0
  187. package/src/tools/catalog.json +49 -0
  188. package/src/tools/cli-delegate.js +44 -0
  189. package/src/tools/mcp-client.js +106 -0
  190. package/src/tools/micro/cancel-task.js +6 -0
  191. package/src/tools/micro/complete-task.js +6 -0
  192. package/src/tools/micro/fail-task.js +6 -0
  193. package/src/tools/micro/http-fetch.js +74 -0
  194. package/src/tools/micro/index.js +36 -0
  195. package/src/tools/micro/lcm-recall.js +60 -0
  196. package/src/tools/micro/list-dir.js +17 -0
  197. package/src/tools/micro/list-skills.js +46 -0
  198. package/src/tools/micro/load-skill.js +38 -0
  199. package/src/tools/micro/memory-search.js +45 -0
  200. package/src/tools/micro/read-file.js +11 -0
  201. package/src/tools/micro/session-search.js +54 -0
  202. package/src/tools/micro/shell-exec.js +43 -0
  203. package/src/tools/micro/trigger-job.js +79 -0
  204. package/src/tools/micro/web-search.js +58 -0
  205. package/src/tools/micro/workspace-paths.js +39 -0
  206. package/src/tools/micro/write-file.js +14 -0
  207. package/src/tools/micro/write-memory.js +41 -0
  208. package/src/tools/registry.js +348 -0
  209. package/src/tools/tool-result-contract.js +36 -0
  210. package/src/tui/chat.js +835 -0
  211. package/src/tui/renderer.js +175 -0
  212. package/src/tui/socket-client.js +217 -0
  213. package/src/utils/canonical-json.js +29 -0
  214. package/src/utils/compaction.js +30 -0
  215. package/src/utils/env-loader.js +5 -0
  216. package/src/utils/errors.js +80 -0
  217. package/src/utils/fs.js +101 -0
  218. package/src/utils/ids.js +5 -0
  219. package/src/utils/model-context-limits.js +30 -0
  220. package/src/utils/token-budget.js +74 -0
  221. package/src/utils/usage-cost.js +25 -0
  222. package/src/utils/usage-metrics.js +14 -0
  223. package/vendor/smol-toml-1.5.2.tgz +0 -0
@@ -0,0 +1,2763 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import {
6
+ notifyCompaction,
7
+ parseConversationTurns,
8
+ resolveCompactionCondensedFanout,
9
+ resolveCompactionThreshold,
10
+ } from "./utils/compaction.js";
11
+ import { MemoryStore } from "./memory/memory-store.js";
12
+ import { buildTaskPacket } from "./runtime/task-packet.js";
13
+ import { JobRunner } from "./jobs/job-runner.js";
14
+ import { OpenClawShadowBridge } from "./shadow/bridge.js";
15
+ import { ProviderRegistry } from "./providers/registry.js";
16
+ import { Scheduler } from "./runtime/scheduler.js";
17
+ import { Executor } from "./runtime/executor.js";
18
+ import { SchedulerDaemon, acquireDaemonLifecycleLock } from "./runtime/daemon.js";
19
+ import { RunReviewer } from "./runtime/review.js";
20
+ import { Evaluator } from "./runtime/evaluation.js";
21
+ import { EmbeddingService } from "./memory/embedding-service.js";
22
+ import { EmbeddingIndex } from "./memory/embedding-index.js";
23
+ import { DeliveryManager } from "./runtime/delivery-manager.js";
24
+ import { StandaloneTransportServer } from "./runtime/transport-server.js";
25
+ import { PeerRegistry } from "./runtime/peer-registry.js";
26
+ import { RunStore } from "./runtime/run-store.js";
27
+ import { NotificationStore } from "./runtime/notification-store.js";
28
+ import { DeliveryStore } from "./runtime/delivery-store.js";
29
+ import { buildRetentionPolicy, pruneJsonBuckets } from "./runtime/retention.js";
30
+ import { ImprovementHarness } from "./runtime/improvement-harness.js";
31
+ import { getReportFallbackPolicy } from "./runtime/report-fallback.js";
32
+ import { buildLaneReadiness } from "./runtime/lane-readiness.js";
33
+ import { DependencyHealth } from "./runtime/dependency-health.js";
34
+ import { PeerReadinessProbe } from "./runtime/peer-readiness.js";
35
+ import { buildPilotStatus } from "./runtime/pilot-status.js";
36
+ import { buildCutoverReadiness } from "./runtime/cutover-readiness.js";
37
+ import { validateAllConfigs } from "./config/schema-validator.js";
38
+ import { buildRuntimeStatus, formatRuntimeStatus } from "./runtime/status-aggregator.js";
39
+ import { resolveAlias } from "./onboarding/aliases.js";
40
+ import { registerWebhook, whoami, getMe } from "./runtime/telegram-inbound.js";
41
+ import { formatCommandHelp, formatGeneralHelp, hasHelpFlag, isKnownCommand, shouldShowAllCommands } from "./cli/help.js";
42
+ import {
43
+ MCP_EMPTY_STATE_MESSAGE,
44
+ getDaemonPaths,
45
+ isPidRunning,
46
+ listServeDaemonPids,
47
+ restartDaemonCommand,
48
+ readTrackedPid,
49
+ runLogsCommand,
50
+ startDaemonCommand,
51
+ stopDaemonCommand,
52
+ } from "./cli/runtime-control.js";
53
+ import { resolveAuthProfilesPath, resolveInstallDir } from "./auth/auth-profiles.js";
54
+
55
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
56
+ const projectRoot = path.join(__dirname, "..");
57
+ const stateRoot = path.join(projectRoot, "state", "memory");
58
+ const liveRoot = resolveLiveRoot(projectRoot);
59
+ const scheduler = new Scheduler({
60
+ projectRoot,
61
+ liveRoot,
62
+ stateRoot: path.join(projectRoot, "state")
63
+ });
64
+ const executor = new Executor({
65
+ projectRoot,
66
+ liveRoot,
67
+ stateRoot: path.join(projectRoot, "state")
68
+ });
69
+ const daemon = new SchedulerDaemon({
70
+ projectRoot,
71
+ liveRoot,
72
+ stateRoot: path.join(projectRoot, "state")
73
+ });
74
+ const reviewer = new RunReviewer({
75
+ stateRoot: path.join(projectRoot, "state")
76
+ });
77
+ const deliveryManager = new DeliveryManager({
78
+ projectRoot,
79
+ liveRoot,
80
+ stateRoot: path.join(projectRoot, "state")
81
+ });
82
+ const evaluator = new Evaluator({
83
+ projectRoot,
84
+ liveRoot,
85
+ stateRoot: path.join(projectRoot, "state")
86
+ });
87
+ const improvementHarness = new ImprovementHarness({
88
+ projectRoot,
89
+ stateRoot: path.join(projectRoot, "state"),
90
+ executor,
91
+ evaluator
92
+ });
93
+ const dependencyHealth = new DependencyHealth({
94
+ projectRoot,
95
+ liveRoot
96
+ });
97
+ const peerReadinessProbe = new PeerReadinessProbe({ liveRoot });
98
+ const embeddingService = new EmbeddingService({ projectRoot });
99
+ const embeddingIndex = new EmbeddingIndex({
100
+ rootDir: path.join(projectRoot, "state", "memory"),
101
+ embeddingService
102
+ });
103
+
104
+ function resolveLiveRoot(projectRoot) {
105
+ if (process.env.NEMORIS_STANDALONE === "1" || process.env.NEMORIS_STANDALONE === "true") {
106
+ return null;
107
+ }
108
+ const explicit = process.env.NEMORIS_LIVE_ROOT;
109
+ if (!explicit) {
110
+ const homedir = process.env.HOME || os.homedir();
111
+ return path.join(homedir, ".openclaw");
112
+ }
113
+ return path.isAbsolute(explicit) ? explicit : path.resolve(projectRoot, explicit);
114
+ }
115
+
116
+ function parseArgs(argv) {
117
+ const [command, ...rest] = argv.slice(2);
118
+ return { command, rest };
119
+ }
120
+
121
+ function parseFlagValue(args, flag) {
122
+ const index = args.indexOf(flag);
123
+ if (index === -1) return null;
124
+ const value = args[index + 1];
125
+ if (!value || value.startsWith("--")) return null;
126
+ return value;
127
+ }
128
+
129
+ function resolveRuntimeInstallDir() {
130
+ return resolveInstallDir({ env: process.env, cwd: process.cwd() });
131
+ }
132
+
133
+ async function seedDemo(memoryStore) {
134
+ await memoryStore.appendEvent("main", {
135
+ title: "heartbeat status",
136
+ content: "Heartbeat found no urgent notifications but token budget was tight in the morning run.",
137
+ category: "health_summary",
138
+ salience: 0.52
139
+ });
140
+
141
+ await memoryStore.writeSummary("main", {
142
+ title: "memory rollout decision",
143
+ summary: "Use policy-gated durable memory with checkpoints instead of transcript compaction.",
144
+ content: "The system should store facts and summaries separately and keep job loops resumable.",
145
+ category: "decision",
146
+ salience: 0.9
147
+ });
148
+
149
+ await memoryStore.writeFact(
150
+ "main",
151
+ {
152
+ title: "local model lane",
153
+ content: "Use local Ollama models for heartbeat, extraction, formatting, and cheap cron work only.",
154
+ category: "workflow_rule",
155
+ reason: "Preserves cost control without giving sensitive orchestration to a weak model.",
156
+ sourceRefs: ["ARCHITECTURE.md"]
157
+ },
158
+ {
159
+ allow_durable_writes: true,
160
+ require_source_reference: true,
161
+ require_write_reason: true,
162
+ max_writes_per_run: 5,
163
+ categories: {
164
+ allowed: ["workflow_rule", "decision"],
165
+ blocked: ["ephemeral_chatter"]
166
+ }
167
+ }
168
+ );
169
+
170
+ await memoryStore.putScratchpad("main", "current-build", {
171
+ title: "open tasks",
172
+ content: "Implement provider adapters after the memory-first core is stable.",
173
+ category: "pending_work",
174
+ expiresAt: new Date(Date.now() + 12 * 60 * 60 * 1000).toISOString(),
175
+ salience: 0.65
176
+ });
177
+
178
+ await memoryStore.saveCheckpoint("main", "latest", {
179
+ objective: "Build the parallel-safe V2 runtime",
180
+ status: "in_progress",
181
+ nextActions: ["add provider adapters", "build shadow-mode bridge"],
182
+ blockers: []
183
+ });
184
+ }
185
+
186
+ async function runDemo() {
187
+ const memoryStore = new MemoryStore({ rootDir: stateRoot });
188
+ await seedDemo(memoryStore);
189
+
190
+ const packet = await buildTaskPacket({
191
+ agentId: "main",
192
+ objective: "Design a heartbeat and memory rollup system with low token overhead",
193
+ memoryStore,
194
+ allowedTools: ["read_file", "list_dir", "run_tests"],
195
+ artifactRefs: ["ARCHITECTURE.md", "MEMORY_SYSTEM.md"],
196
+ budget: {
197
+ maxTokens: 6000,
198
+ maxRuntimeSeconds: 45
199
+ }
200
+ });
201
+
202
+ const runner = new JobRunner({ memoryStore });
203
+ const result = await runner.run(
204
+ {
205
+ id: "workspace-health",
206
+ agentId: "main",
207
+ taskType: "workspace_health",
208
+ budget: {
209
+ maxTokens: 2000,
210
+ maxRuntimeSeconds: 15
211
+ }
212
+ },
213
+ async () => ({
214
+ summary: "Workspace health demo completed without touching live OpenClaw state.",
215
+ output: "Demo run confirmed that the memory-first runtime can checkpoint and summarize job results.",
216
+ nextActions: ["implement provider adapters"]
217
+ })
218
+ );
219
+
220
+ console.log(JSON.stringify({ packet, result }, null, 2));
221
+ }
222
+
223
+ async function inspectMemory(agentId, query) {
224
+ const memoryStore = new MemoryStore({ rootDir: stateRoot });
225
+ const result = await memoryStore.query(agentId, query, { limit: 10 });
226
+ console.log(JSON.stringify(result, null, 2));
227
+ }
228
+
229
+ async function shadowSummary(agentId) {
230
+ const bridge = new OpenClawShadowBridge({ liveRoot });
231
+ const summary = await bridge.buildWorkspaceSnapshot(agentId);
232
+ console.log(
233
+ JSON.stringify(
234
+ {
235
+ agentId,
236
+ docs: summary.docs.map((doc) => doc.fullPath),
237
+ recentMemory: summary.recentMemory.map((entry) => entry.filePath),
238
+ sessions: summary.sessionEntries.slice(0, 5)
239
+ },
240
+ null,
241
+ 2
242
+ )
243
+ );
244
+ }
245
+
246
+ async function shadowImport(agentId) {
247
+ const memoryStore = new MemoryStore({ rootDir: stateRoot });
248
+ const bridge = new OpenClawShadowBridge({ liveRoot });
249
+ const policy = {
250
+ allow_durable_writes: true,
251
+ require_source_reference: true,
252
+ require_write_reason: true,
253
+ max_writes_per_run: 5,
254
+ categories: {
255
+ allowed: ["artifact_summary"],
256
+ blocked: ["ephemeral_chatter", "raw_tool_output"]
257
+ }
258
+ };
259
+ const result = await bridge.importWorkspaceSnapshot(agentId, memoryStore, policy);
260
+ console.log(
261
+ JSON.stringify(
262
+ {
263
+ agentId,
264
+ importedDocs: result.docs.length,
265
+ importedMemoryFiles: result.recentMemory.length
266
+ },
267
+ null,
268
+ 2
269
+ )
270
+ );
271
+ }
272
+
273
+ async function providerHealth(providerId) {
274
+ const registry = new ProviderRegistry();
275
+ const providerConfigs = {
276
+ anthropic: {
277
+ id: "anthropic",
278
+ adapter: "anthropic",
279
+ authRef: "env:NEMORIS_ANTHROPIC_API_KEY",
280
+ baseUrl: "https://api.anthropic.com"
281
+ },
282
+ openrouter: {
283
+ id: "openrouter",
284
+ adapter: "openrouter",
285
+ authRef: "env:OPENROUTER_API_KEY",
286
+ baseUrl: "https://openrouter.ai/api/v1",
287
+ appName: "Nemoris V2"
288
+ },
289
+ ollama: {
290
+ id: "ollama",
291
+ adapter: "ollama",
292
+ authRef: "env:OLLAMA_API_KEY",
293
+ baseUrl: "http://127.0.0.1:11434"
294
+ }
295
+ };
296
+
297
+ const adapter = registry.create(providerConfigs[providerId]);
298
+ const result = await adapter.healthCheck();
299
+ console.log(JSON.stringify(result, null, 2));
300
+ }
301
+
302
+ async function providerCapabilities(providerId) {
303
+ const runtime = await scheduler.loadRuntime();
304
+ const providerConfig = runtime.providers[providerId];
305
+ if (!providerConfig) {
306
+ throw new Error(`Unknown provider: ${providerId}`);
307
+ }
308
+
309
+ const registry = new ProviderRegistry();
310
+ const result = registry.describe(providerConfig);
311
+ console.log(JSON.stringify(result, null, 2));
312
+ }
313
+
314
+ async function manifestSummary() {
315
+ const runtime = await scheduler.loadRuntime();
316
+ console.log(
317
+ JSON.stringify(
318
+ {
319
+ providers: Object.keys(runtime.providers),
320
+ agents: Object.keys(runtime.agents),
321
+ jobs: Object.keys(runtime.jobs),
322
+ policies: Object.keys(runtime.policies),
323
+ lanes: Object.keys(runtime.router)
324
+ },
325
+ null,
326
+ 2
327
+ )
328
+ );
329
+ }
330
+
331
+ async function planJob(jobId) {
332
+ const plan = await scheduler.hydrateJob(jobId, { shadowImport: true });
333
+ console.log(JSON.stringify(plan, null, 2));
334
+ }
335
+
336
+ async function liveCronSummary() {
337
+ const summary = await scheduler.summarizeLiveCron();
338
+ console.log(JSON.stringify(summary, null, 2));
339
+ }
340
+
341
+ async function compareJobs() {
342
+ const comparison = await scheduler.compareJobs();
343
+ console.log(JSON.stringify(comparison, null, 2));
344
+ }
345
+
346
+ async function executeJob(jobId, mode = "dry-run", modelOverride = null) {
347
+ const result = await executor.executeJob(jobId, {
348
+ mode,
349
+ shadowImport: true,
350
+ modelOverride
351
+ });
352
+ console.log(
353
+ JSON.stringify(
354
+ {
355
+ filePath: result.filePath,
356
+ mode: result.mode,
357
+ providerId: result.providerId,
358
+ modelId: result.modelId,
359
+ routingDecision: result.routingDecision,
360
+ fallback: result.fallback || null,
361
+ providerHealth: result.providerHealth,
362
+ preflight: result.preflight,
363
+ summary: result.result.summary,
364
+ nextActions: result.result.nextActions,
365
+ notificationFiles: result.notificationFiles || []
366
+ },
367
+ null,
368
+ 2
369
+ )
370
+ );
371
+ }
372
+
373
+ async function executeJobWithReportFallback(jobId, mode = "provider", modelOverride = null) {
374
+ const result = await executor.executeJob(jobId, {
375
+ mode,
376
+ shadowImport: true,
377
+ modelOverride,
378
+ allowReportFallback: true
379
+ });
380
+ console.log(
381
+ JSON.stringify(
382
+ {
383
+ filePath: result.filePath,
384
+ mode: result.mode,
385
+ providerId: result.providerId,
386
+ modelId: result.modelId,
387
+ routingDecision: result.routingDecision,
388
+ fallback: result.fallback || null,
389
+ providerHealth: result.providerHealth,
390
+ preflight: result.preflight,
391
+ summary: result.result.summary,
392
+ nextActions: result.result.nextActions,
393
+ notificationFiles: result.notificationFiles || []
394
+ },
395
+ null,
396
+ 2
397
+ )
398
+ );
399
+ }
400
+
401
+ async function shadowCompare(jobId) {
402
+ const comparison = await executor.shadowCompare(jobId);
403
+ console.log(JSON.stringify(comparison, null, 2));
404
+ }
405
+
406
+ async function providerModePolicy() {
407
+ const { getProviderModePolicy } = await import("./runtime/guards.js");
408
+ console.log(JSON.stringify(getProviderModePolicy(), null, 2));
409
+ }
410
+
411
+ async function tickScheduler(mode = "dry-run") {
412
+ const result = await daemon.tick({
413
+ now: new Date(),
414
+ mode
415
+ });
416
+ console.log(JSON.stringify(result, null, 2));
417
+ }
418
+
419
+ async function dueJobs() {
420
+ const jobs = await daemon.listDueJobs(new Date());
421
+ console.log(JSON.stringify(jobs, null, 2));
422
+ }
423
+
424
+ async function reviewRuns(limit = 10) {
425
+ const review = await reviewer.review(Number(limit));
426
+ console.log(JSON.stringify(review, null, 2));
427
+ }
428
+
429
+ async function reviewNotifications(limit = 10) {
430
+ const notifications = await reviewer.notificationStore.listRecent(Number(limit));
431
+ console.log(JSON.stringify(notifications, null, 2));
432
+ }
433
+
434
+ async function reviewDeliveries(limit = 10) {
435
+ const deliveries = await reviewer.deliveryStore.listRecent(Number(limit));
436
+ console.log(JSON.stringify(deliveries, null, 2));
437
+ }
438
+
439
+ async function reviewPendingHandoffs(limit = 10) {
440
+ const handoffs = await deliveryManager.listPendingHandoffs(Number(limit));
441
+ console.log(JSON.stringify(handoffs, null, 2));
442
+ }
443
+
444
+ async function sweepPendingHandoffs() {
445
+ const runtime = await scheduler.loadRuntime();
446
+ const result = await deliveryManager.sweepPendingHandoffs({
447
+ timeoutMinutes: runtime.runtime?.handoffs?.pendingTimeoutMinutes,
448
+ escalateOnExpiry: runtime.runtime?.handoffs?.escalateOnExpiry,
449
+ escalationDeliveryProfile: runtime.runtime?.handoffs?.escalationDeliveryProfile
450
+ });
451
+ console.log(JSON.stringify(result, null, 2));
452
+ }
453
+
454
+ async function deliverNotifications(mode = "shadow", limit = "10") {
455
+ const result = await deliveryManager.deliverPending({
456
+ mode,
457
+ limit: Number(limit)
458
+ });
459
+ console.log(JSON.stringify(result, null, 2));
460
+ }
461
+
462
+ async function deliverNotificationFile(mode = "shadow", notificationFilePath = null) {
463
+ if (!notificationFilePath) {
464
+ throw new Error("deliver-notification-file requires <mode> <notification-file-path>");
465
+ }
466
+ const resolvedPath = path.isAbsolute(notificationFilePath)
467
+ ? notificationFilePath
468
+ : path.resolve(projectRoot, notificationFilePath);
469
+ const result = await deliveryManager.deliverPending({
470
+ mode,
471
+ limit: 1,
472
+ notificationFiles: [resolvedPath]
473
+ });
474
+ console.log(JSON.stringify(result, null, 2));
475
+ }
476
+
477
+ async function maintenanceStatus() {
478
+ const state = await daemon.stateStore.getMeta("maintenance", null);
479
+ console.log(JSON.stringify(state, null, 2));
480
+ }
481
+
482
+ async function resolveDeliveryProfile(profileName = "gateway_preview_main") {
483
+ const runtime = await scheduler.loadRuntime();
484
+ const profile = await deliveryManager.resolveProfileConfig(
485
+ profileName,
486
+ deliveryManager.resolveProfileEntry(runtime.delivery, profileName) || {}
487
+ );
488
+ console.log(JSON.stringify(profile, null, 2));
489
+ }
490
+
491
+ async function sendTestDelivery(profileName = "gateway_preview_main", mode = "shadow", ...messageParts) {
492
+ const message =
493
+ messageParts.join(" ").trim() ||
494
+ `Nemoris V2 delivery test · ${new Date().toISOString()} · mode=${mode} · profile=${profileName}`;
495
+ const result = await deliveryManager.sendTestDelivery({
496
+ profileName,
497
+ mode,
498
+ message
499
+ });
500
+ console.log(JSON.stringify(result, null, 2));
501
+ }
502
+
503
+ async function chooseHandoffPeer(notificationFilePath, peerId, deliveryProfile = null) {
504
+ if (!notificationFilePath || !peerId) {
505
+ throw new Error("choose-handoff-peer requires <notification-file-path> and <peer-id>");
506
+ }
507
+ const resolvedPath = path.isAbsolute(notificationFilePath)
508
+ ? notificationFilePath
509
+ : path.resolve(projectRoot, notificationFilePath);
510
+ const result = await deliveryManager.chooseHandoffPeer(resolvedPath, peerId, {
511
+ deliveryProfile
512
+ });
513
+ console.log(JSON.stringify(result, null, 2));
514
+ }
515
+
516
+ async function chooseTopHandoffPeer(notificationFilePath, deliveryProfile = null) {
517
+ if (!notificationFilePath) {
518
+ throw new Error("choose-top-handoff-peer requires <notification-file-path>");
519
+ }
520
+ const resolvedPath = path.isAbsolute(notificationFilePath)
521
+ ? notificationFilePath
522
+ : path.resolve(projectRoot, notificationFilePath);
523
+ const result = await deliveryManager.chooseTopSuggestedPeer(resolvedPath, {
524
+ deliveryProfile
525
+ });
526
+ console.log(JSON.stringify(result, null, 2));
527
+ }
528
+
529
+ async function serveTransport(port = "4318") {
530
+ const authToken = process.env.NEMORIS_TRANSPORT_TOKEN || null;
531
+ const runtime = await scheduler.loadRuntime();
532
+ const server = new StandaloneTransportServer({
533
+ stateRoot: path.join(projectRoot, "state"),
534
+ authToken,
535
+ retention: runtime.runtime?.retention?.transportInbox || {},
536
+ requestTimeoutMs: runtime.runtime?.shutdown?.transportShutdownTimeoutMs || 5000
537
+ });
538
+ // Attach Telegram inbound if configured
539
+ const telegramConfig = runtime.runtime?.telegram;
540
+ if (telegramConfig?.operatorChatId) {
541
+ const schedulerStateStore = daemon.stateStore;
542
+ await schedulerStateStore.ensureReady();
543
+ const peerNames = Object.keys(runtime.peers?.peers || {});
544
+ const agentNames = Object.fromEntries(
545
+ Object.entries(runtime.agents || {}).map(([id, agent]) => [id, agent?.name || id])
546
+ );
547
+ server.attachTelegramInbound({
548
+ stateStore: schedulerStateStore,
549
+ telegramConfig,
550
+ availablePeers: peerNames,
551
+ contextLedger: null,
552
+ agentNames,
553
+ routerConfig: runtime.router,
554
+ agentConfigs: runtime.agents,
555
+ });
556
+ const execApprovalGate = server.telegramHandler?.getExecApprovalGate?.();
557
+ daemon.executor.setExecApprovalGate(execApprovalGate);
558
+ executor.setExecApprovalGate(execApprovalGate);
559
+ }
560
+
561
+ // Attach Slack inbound if configured
562
+ const slackConfig = runtime.runtime?.slack;
563
+ if (slackConfig?.enabled) {
564
+ const schedulerStateStore = daemon.stateStore;
565
+ await schedulerStateStore.ensureReady();
566
+ const peerNames = Object.keys(runtime.peers?.peers || {});
567
+ server.attachSlackInbound({
568
+ stateStore: schedulerStateStore,
569
+ slackConfig,
570
+ availablePeers: peerNames,
571
+ });
572
+ }
573
+
574
+ const address = await server.listen(Number(port) || 4318, "127.0.0.1");
575
+ const shutdown = async () => {
576
+ await server.close();
577
+ process.exit(0);
578
+ };
579
+ process.once("SIGINT", shutdown);
580
+ process.once("SIGTERM", shutdown);
581
+ console.log(
582
+ JSON.stringify(
583
+ {
584
+ ok: true,
585
+ transport: "standalone_http",
586
+ address,
587
+ authRequired: Boolean(authToken)
588
+ },
589
+ null,
590
+ 2
591
+ )
592
+ );
593
+ }
594
+
595
+ async function setupTelegram() {
596
+ const runtime = await scheduler.loadRuntime();
597
+ const telegramConfig = runtime.runtime?.telegram || {};
598
+
599
+ const tokenEnvName = telegramConfig.botTokenEnv || "NEMORIS_TELEGRAM_BOT_TOKEN";
600
+ const token = process.env[tokenEnvName];
601
+ if (!token) {
602
+ console.log(`\n❌ ${tokenEnvName} is not set.`);
603
+ console.log(` Get a token from @BotFather on Telegram and set it in your environment.`);
604
+ console.log(` Example: export ${tokenEnvName}=your-bot-token\n`);
605
+ process.exitCode = 1;
606
+ return;
607
+ }
608
+
609
+ // Validate token against Telegram API
610
+ const me = await getMe(token);
611
+ if (me.ok) {
612
+ console.log(`✅ Token valid — @${me.username}`);
613
+ } else {
614
+ console.log(`❌ Token invalid: ${me.error}`);
615
+ process.exitCode = 1;
616
+ return;
617
+ }
618
+
619
+ if (telegramConfig.operatorChatId) {
620
+ console.log(`✅ Operator chat ID: ${telegramConfig.operatorChatId}`);
621
+ } else {
622
+ console.log(`\n⚠️ telegram.operator_chat_id is not set in config/runtime.toml.`);
623
+ console.log(` Run: nemoris telegram whoami`);
624
+ console.log(` Then add your chat_id to config/runtime.toml.\n`);
625
+ }
626
+
627
+ if (telegramConfig.pollingMode) {
628
+ console.log(`✅ Mode: long polling (no webhook needed)`);
629
+ return;
630
+ }
631
+
632
+ const webhookUrl = telegramConfig.webhookUrl;
633
+ if (!webhookUrl) {
634
+ console.log(`\n❌ telegram.webhook_url is not configured in config/runtime.toml.`);
635
+ console.log(` You need a public URL for webhooks, or switch to polling_mode = true.\n`);
636
+ process.exitCode = 1;
637
+ return;
638
+ }
639
+
640
+ console.log(`\nRegistering webhook...`);
641
+ const result = await registerWebhook(token, webhookUrl);
642
+ if (result.ok) {
643
+ console.log(`✅ Webhook registered: ${webhookUrl}/telegram/webhook`);
644
+ } else {
645
+ console.log(`❌ Webhook registration failed: ${result.error}`);
646
+ process.exitCode = 1;
647
+ }
648
+ }
649
+
650
+ function formatModelsOverview(overview) {
651
+ const lines = [
652
+ "Auth overview",
653
+ "─────────────────────────────────────────",
654
+ ];
655
+
656
+ for (const entry of overview.authLines || []) {
657
+ lines.push(` ${entry.provider.padEnd(13)} ${entry.ok ? "✓" : "✗"} ${entry.method} ${entry.detail}`);
658
+ }
659
+
660
+ lines.push("");
661
+ lines.push("Models");
662
+ lines.push("─────────────────────────────────────────");
663
+ lines.push(` Default : ${overview.defaultModel || "not configured"}`);
664
+ lines.push(` Fallback : ${overview.fallbackModel || "not configured"}`);
665
+ lines.push(` Local : ${overview.localModel || "not configured"}`);
666
+ return `${lines.join("\n")}\n`;
667
+ }
668
+
669
+ async function showModelsOverview(installDir = resolveRuntimeInstallDir()) {
670
+ const loader = new ConfigLoader({ rootDir: path.join(installDir, "config") });
671
+ const [{ buildModelsOverview }, router, providers] = await Promise.all([
672
+ import("./onboarding/model-catalog.js"),
673
+ loader.loadRouter(),
674
+ loader.loadProviders(),
675
+ ]);
676
+
677
+ const overview = buildModelsOverview({
678
+ router,
679
+ providers,
680
+ authProfilesPath: resolveAuthProfilesPath({ env: process.env, cwd: process.cwd() }),
681
+ });
682
+ writeOutput(process.stdout, formatModelsOverview(overview));
683
+ }
684
+
685
+ async function telegramWhoami() {
686
+ const runtime = await scheduler.loadRuntime();
687
+ const telegramConfig = runtime.runtime?.telegram || {};
688
+ const tokenEnvName = telegramConfig.botTokenEnv || "NEMORIS_TELEGRAM_BOT_TOKEN";
689
+ const token = process.env[tokenEnvName];
690
+ if (!token) {
691
+ console.log(`❌ ${tokenEnvName} is not set. Cannot query Telegram.`);
692
+ process.exitCode = 1;
693
+ return;
694
+ }
695
+
696
+ console.log("Querying Telegram for your chat info...");
697
+ const result = await whoami(token, { webhookUrl: telegramConfig.webhookUrl || null });
698
+ if (!result) {
699
+ console.log("No messages found. Send a message to your bot first, then re-run this command.");
700
+ return;
701
+ }
702
+ console.log(`\n chat_id: ${result.chatId}`);
703
+ console.log(` username: ${result.username || "(not set)"}`);
704
+ console.log(` first_name: ${result.firstName || "(not set)"}`);
705
+ console.log(`\nAdd this to config/runtime.toml:`);
706
+ console.log(` operator_chat_id = "${result.chatId}"\n`);
707
+ }
708
+
709
+ async function serveDaemon(mode = "dry-run", intervalMs = "30000", installDir = null) {
710
+ const root = installDir || resolveRuntimeInstallDir();
711
+ const rootLiveRoot = resolveLiveRoot(root);
712
+ const stateDir = path.join(root, "state");
713
+
714
+ // Construct fresh instances scoped to the correct install dir —
715
+ // module-level singletons use __dirname which points to the npm
716
+ // package dir, not the user's install dir.
717
+ const localScheduler = new Scheduler({
718
+ projectRoot: root,
719
+ liveRoot: rootLiveRoot,
720
+ stateRoot: stateDir
721
+ });
722
+ const localExecutor = new Executor({
723
+ projectRoot: root,
724
+ liveRoot: rootLiveRoot,
725
+ stateRoot: stateDir
726
+ });
727
+ const localDaemon = new SchedulerDaemon({
728
+ projectRoot: root,
729
+ liveRoot: rootLiveRoot,
730
+ stateRoot: stateDir
731
+ });
732
+ // Wire executor into daemon
733
+ localDaemon.executor = localExecutor;
734
+
735
+ const runtime = await localScheduler.loadRuntime();
736
+ fs.mkdirSync(stateDir, { recursive: true });
737
+
738
+ // Acquire exclusive daemon lock — prevents multiple daemon processes from running simultaneously.
739
+ const lockResult = acquireDaemonLifecycleLock({ stateDir });
740
+ if (!lockResult || !lockResult.release) {
741
+ // Lock held by another process — acquireDaemonLifecycleLock already printed error.
742
+ process.exit(1);
743
+ }
744
+ const { pidFile, release: releaseLock } = lockResult;
745
+
746
+ const removePidFile = () => {
747
+ try {
748
+ releaseLock();
749
+ } catch {
750
+ // Best effort cleanup.
751
+ }
752
+ };
753
+
754
+ process.once("exit", removePidFile);
755
+
756
+ // Phase 1: Startup Banner (shadow read)
757
+ if (process.env.NEMORIS_LIVE_ROOT) {
758
+ const { OpenClawShadowBridge } = await import("./shadow/bridge.js");
759
+ const bridge = new OpenClawShadowBridge({ liveRoot: process.env.NEMORIS_LIVE_ROOT });
760
+ try {
761
+ const agents = await bridge.listAgents();
762
+ const cronJobs = await bridge.loadCronJobs();
763
+ console.log(JSON.stringify({
764
+ service: "nemoris_startup",
765
+ event: "openclaw_detected",
766
+ agentCount: agents.length,
767
+ cronJobCount: cronJobs.length,
768
+ message: `OpenClaw detected at ${process.env.NEMORIS_LIVE_ROOT} — ${agents.length} agents, ${cronJobs.length} cron jobs available. Run 'nemoris migrate' to import.`
769
+ }));
770
+ } catch (_) {}
771
+ }
772
+
773
+ const drainTimeoutMs = runtime.runtime?.shutdown?.drainTimeoutMs || 15000;
774
+ const pollIntervalMs = Math.max(1000, Number(intervalMs) || 30000);
775
+ let timer = null;
776
+
777
+ const runTick = async () => {
778
+ try {
779
+ const result = await localDaemon.tick({
780
+ now: new Date(),
781
+ mode
782
+ });
783
+ console.log(JSON.stringify(result, null, 2));
784
+ } catch (error) {
785
+ console.error(
786
+ JSON.stringify({ ok: false, service: "scheduler_daemon", error: error.message })
787
+ );
788
+ }
789
+ };
790
+
791
+ // Telegram inbound — polling mode (no tunnel) or webhook mode
792
+ const stateStore = localDaemon.stateStore;
793
+ await stateStore.ensureReady();
794
+ localDaemon.installGlobalHandlers();
795
+ // Wire state store into tool registry for tool result caching
796
+ localDaemon.executor.toolRegistry.setStateStore(stateStore);
797
+
798
+ // Wire MCP consumer if config exists
799
+ if (runtime.mcp?.servers && Object.keys(runtime.mcp.servers).length > 0) {
800
+ const { McpConsumer } = await import("./mcp/consumer.js");
801
+ const mcpConsumer = new McpConsumer({ config: runtime.mcp });
802
+ localDaemon.setMcpConsumer(mcpConsumer);
803
+ localDaemon.executor.toolRegistry.setMcpConsumer(mcpConsumer);
804
+ console.log(JSON.stringify({ service: "mcp_consumer", status: "READY", servers: Object.keys(runtime.mcp.servers) }));
805
+ }
806
+
807
+ // Reap jobs stuck in 'running' state from a previous daemon run
808
+ const { reapOrphanedJobs } = await import("./runtime/orphan-reaper.js");
809
+ reapOrphanedJobs(stateStore);
810
+
811
+ // Context ledger — event-sourced session store (Phase 1: dual-write)
812
+ const { ContextLedger } = await import("./runtime/context-ledger.js");
813
+ const contextLedger = new ContextLedger({
814
+ dbPath: path.join(root, "state", "context-ledger.sqlite"),
815
+ });
816
+ contextLedger.open();
817
+
818
+ const telegramConfig = runtime.runtime?.telegram;
819
+ let telegramPoller = null;
820
+
821
+ if (telegramConfig?.botTokenEnv) {
822
+ const token = process.env[telegramConfig.botTokenEnv];
823
+ if (token && telegramConfig.pollingMode) {
824
+ // Long-polling mode — no tunnel or transport server required
825
+ const { startTelegramPolling, TelegramInboundHandler } = await import("./runtime/telegram-inbound.js");
826
+ const peerNames = Object.keys(runtime.peers?.peers || {});
827
+ const agentNames = Object.fromEntries(
828
+ Object.entries(runtime.agents || {}).map(([id, agent]) => [id, agent?.name || id])
829
+ );
830
+ const handler = new TelegramInboundHandler({
831
+ stateStore,
832
+ telegramConfig,
833
+ availablePeers: peerNames,
834
+ agentNames,
835
+ contextLedger,
836
+ routerConfig: runtime.router,
837
+ agentConfigs: runtime.agents,
838
+ stopPolling: () => telegramPoller?.stop(),
839
+ });
840
+ const execApprovalGate = handler.getExecApprovalGate();
841
+ localDaemon.executor.setExecApprovalGate(execApprovalGate);
842
+ localExecutor.setExecApprovalGate(execApprovalGate);
843
+ telegramPoller = startTelegramPolling({ botToken: token, handler });
844
+ await stateStore.setMeta("telegram_inbound", { status: "READY", mode: "polling" });
845
+ console.log(JSON.stringify({ service: "telegram_inbound", status: "READY", mode: "polling" }));
846
+ } else if (token && telegramConfig.webhookUrl) {
847
+ // Webhook mode — requires tunnel + transport server
848
+ const webhookResult = await registerWebhook(token, telegramConfig.webhookUrl);
849
+ if (webhookResult.ok) {
850
+ await stateStore.setMeta("telegram_inbound", { status: "READY", webhook: `${telegramConfig.webhookUrl}/telegram/webhook` });
851
+ console.log(JSON.stringify({ service: "telegram_inbound", status: "READY", webhook: `${telegramConfig.webhookUrl}/telegram/webhook` }));
852
+ } else {
853
+ await stateStore.setMeta("telegram_inbound", { status: "NOT_READY", error: webhookResult.error });
854
+ console.error(JSON.stringify({ service: "telegram_inbound", status: "NOT_READY", error: webhookResult.error }));
855
+ }
856
+ } else if (!token) {
857
+ await stateStore.setMeta("telegram_inbound", { status: "NOT_READY", error: `${telegramConfig.botTokenEnv} not set` });
858
+ console.error(JSON.stringify({ service: "telegram_inbound", status: "NOT_READY", error: `${telegramConfig.botTokenEnv} not set` }));
859
+ }
860
+ }
861
+
862
+ const slackConfig = runtime.runtime?.slack;
863
+ let slackHandler = null;
864
+ if (slackConfig?.enabled) {
865
+ const { SlackInboundHandler } = await import("./runtime/slack-inbound.js");
866
+ const peerNames = Object.keys(runtime.peers?.peers || {});
867
+ slackHandler = new SlackInboundHandler({
868
+ stateStore,
869
+ slackConfig,
870
+ availablePeers: peerNames,
871
+ });
872
+ console.log(JSON.stringify({ service: "slack_inbound", status: "READY", eventsPath: slackConfig.eventsPath }));
873
+ }
874
+
875
+ const { TuiServer } = await import("./runtime/tui-server.js");
876
+ let tuiServer = null;
877
+ let tuiDeliveryAdapter = null;
878
+ try {
879
+ tuiServer = new TuiServer({
880
+ stateStore,
881
+ stateRoot: path.join(root, "state"),
882
+ runtime,
883
+ contextLedger,
884
+ execApprovalGate: localDaemon.executor.execApprovalGate,
885
+ });
886
+ await tuiServer.start();
887
+ tuiDeliveryAdapter = localDaemon.deliveryManager.createAdapter("tui");
888
+ console.log(JSON.stringify({ service: "tui_socket", status: "READY", socketPath: tuiServer.socketPath }));
889
+ } catch (error) {
890
+ console.error(JSON.stringify({ service: "tui_socket", status: "NOT_READY", error: error.message }));
891
+ tuiServer = null;
892
+ tuiDeliveryAdapter = null;
893
+ }
894
+
895
+ // Graceful drain helpers
896
+ let shuttingDown = false;
897
+
898
+ const waitForDrainEmpty = async (timeoutMs = drainTimeoutMs) => {
899
+ const deadline = Date.now() + timeoutMs;
900
+ while (Date.now() < deadline) {
901
+ const running = stateStore.getRunningInteractiveJobCount();
902
+ if (running === 0) return;
903
+ await new Promise((r) => setTimeout(r, 100));
904
+ }
905
+ };
906
+
907
+ // Interactive drain loop — 100ms polling, independent of scheduled tick
908
+ let interactiveRunning = true;
909
+ const interactiveDrain = async () => {
910
+ const { drainInteractiveOnce } = await import("./runtime/daemon.js");
911
+ const { appendTurn, processInteractiveHandoffs } = await import("./runtime/telegram-inbound.js");
912
+ const { fireCompletionPings } = await import("./runtime/completion-ping.js");
913
+ const { sendFailurePing } = await import("./runtime/failure-ping.js");
914
+
915
+ while (interactiveRunning) {
916
+ try {
917
+ await drainInteractiveOnce(stateStore, async (job) => {
918
+ // Interactive jobs don't have TOML definitions — hydrate from message + agent config
919
+ const chatSession = stateStore.getChatSession(job.chat_id);
920
+ const { compactSessionContext } = await import("./runtime/session-compactor.js");
921
+
922
+ // Load raw conversation context (JSON string or null)
923
+ const sessionId = job.chat_id || job.job_id;
924
+ let rawContext = (() => {
925
+ try {
926
+ const { snapshot } = contextLedger.loadState(sessionId);
927
+ if (snapshot) {
928
+ console.log(JSON.stringify({ service: "context_ledger", path: "ledger", session_id: sessionId }));
929
+ // Return raw string — hydrateInteractiveJob calls JSON.parse() on it
930
+ return snapshot.state_json;
931
+ }
932
+ } catch (err) {
933
+ console.error(JSON.stringify({ service: "context_ledger", error: err.message, session_id: sessionId }));
934
+ }
935
+ console.log(JSON.stringify({ service: "context_ledger", path: "blob", session_id: sessionId }));
936
+ return chatSession?.conversation_context || null;
937
+ })();
938
+
939
+ // Session compaction — summarise old turns to keep context manageable
940
+ try {
941
+ const agentCfg = runtime.agents?.[job.agent_id];
942
+ const compactionThreshold = resolveCompactionThreshold(agentCfg);
943
+ const condensedFanout = resolveCompactionCondensedFanout(agentCfg);
944
+ const compactionModel = agentCfg?.limits?.compaction_model ?? "ollama/qwen3:8b";
945
+ const transcriptContext = chatSession?.conversation_context || null;
946
+ const turns = parseConversationTurns(transcriptContext);
947
+
948
+ // Pre-compaction memory flush: if approaching threshold, ask agent to write memory
949
+ if (turns.length >= compactionThreshold - 1) {
950
+ import("./runtime/telegram-inbound.js").then(({ appendTurn }) => {
951
+ const flushCtx = appendTurn(transcriptContext, "user",
952
+ "Please write any important facts from our conversation to memory before we continue.");
953
+ daemon.scheduler.hydrateInteractiveJob({
954
+ jobId: `${job.job_id}_memflush`,
955
+ agentId: job.agent_id,
956
+ input: "Please write any important facts from our conversation to memory before we continue.",
957
+ conversationContext: flushCtx,
958
+ }, { shadowImport: false }).catch((err) => {
959
+ console.error(JSON.stringify({ service: "session_compactor", event: "memflush_error", error: err.message }));
960
+ });
961
+ }).catch(() => {});
962
+ }
963
+
964
+ if (turns.length >= compactionThreshold) {
965
+ const turnsBefore = turns.length;
966
+ const noticeToken = runtime.runtime?.telegram?.botTokenEnv
967
+ ? process.env[runtime.runtime.telegram.botTokenEnv]
968
+ : null;
969
+ const noticeChatId = runtime.runtime?.telegram?.operatorChatId || job.chat_id;
970
+ if (noticeToken && noticeChatId && job.source === "telegram") {
971
+ await notifyCompaction(async (chatId, text) => {
972
+ await fetch(`https://api.telegram.org/bot${noticeToken}/sendMessage`, {
973
+ method: "POST",
974
+ headers: { "content-type": "application/json" },
975
+ body: JSON.stringify({ chat_id: chatId, text }),
976
+ }).catch(() => {});
977
+ }, noticeChatId);
978
+ }
979
+ const compacted = await compactSessionContext(turns, {
980
+ maxTurns: compactionThreshold,
981
+ compactionModel,
982
+ condensedFanout,
983
+ ledger: contextLedger,
984
+ sessionId,
985
+ });
986
+ if (compacted.compacted) {
987
+ rawContext = JSON.stringify(compacted.turns);
988
+ console.log(JSON.stringify({
989
+ service: "session_compactor",
990
+ session_id: sessionId,
991
+ turns_before: turnsBefore,
992
+ turns_after: compacted.turns.length,
993
+ summary_length: compacted.summary?.length ?? 0,
994
+ }));
995
+ // Dual-write compacted context back to blob
996
+ if (chatSession) {
997
+ stateStore.updateConversationContext(job.chat_id, rawContext);
998
+ }
999
+ try {
1000
+ contextLedger.appendEvent({
1001
+ session_id: sessionId,
1002
+ kind: "compaction",
1003
+ payload_json: {
1004
+ turns_before: turnsBefore,
1005
+ turns_after: compacted.turns.length,
1006
+ summary: compacted.summary || "",
1007
+ },
1008
+ });
1009
+ } catch (_) {}
1010
+ if (job.chat_id) {
1011
+ return { requeue: true, reason: "compaction" };
1012
+ }
1013
+ }
1014
+ }
1015
+ } catch (compactErr) {
1016
+ // Compaction failure must never break a turn
1017
+ console.error(JSON.stringify({ service: "session_compactor", error: compactErr.message, session_id: sessionId }));
1018
+ }
1019
+
1020
+ const plan = await daemon.scheduler.hydrateInteractiveJob({
1021
+ jobId: job.job_id,
1022
+ agentId: job.agent_id,
1023
+ input: job.input,
1024
+ imageRefs: job.image_refs ? JSON.parse(job.image_refs) : null,
1025
+ conversationContext: rawContext,
1026
+ sessionId: job.chat_id || job.job_id,
1027
+ }, {
1028
+ shadowImport: true,
1029
+ modelOverride: chatSession?.model_override,
1030
+ focusMode: chatSession?.focus_mode || null,
1031
+ thinkMode: chatSession?.think_mode || null,
1032
+ sourceType: job.source_type || 'human',
1033
+ sourceAgentId: job.source_agent_id || null,
1034
+ });
1035
+
1036
+ // Typing indicator — refresh every 5s until executor returns
1037
+ const typingChatId = job.source === "telegram" ? job.chat_id || null : null;
1038
+ const tgCfg = (await daemon.scheduler.loadRuntime()).runtime?.telegram;
1039
+ const typingToken = tgCfg?.botTokenEnv ? process.env[tgCfg.botTokenEnv] : null;
1040
+ let typingTimer = null;
1041
+ if (typingToken && typingChatId) {
1042
+ const sendTyping = () => fetch(`https://api.telegram.org/bot${typingToken}/sendChatAction`, {
1043
+ method: "POST",
1044
+ headers: { "content-type": "application/json" },
1045
+ body: JSON.stringify({ chat_id: typingChatId, action: "typing" }),
1046
+ }).catch(() => {});
1047
+ sendTyping(); // fire immediately
1048
+ typingTimer = setInterval(sendTyping, 5000);
1049
+ }
1050
+
1051
+ const executorMode = mode === "live" ? "provider" : mode;
1052
+
1053
+ // Set per-job tool context (memory_search, trigger_job, web_search)
1054
+ if (daemon.executor._toolContext) {
1055
+ const ctx = daemon.executor._toolContext;
1056
+ ctx.callerAgentId = job.agent_id;
1057
+ ctx.memoryStore = daemon.scheduler.memoryStore;
1058
+ ctx.stateStore = stateStore;
1059
+ ctx.currentJobId = job.job_id;
1060
+ ctx.openRouterApiKey = process.env.OPENROUTER_API_KEY;
1061
+ ctx.agents = runtime.agents || {};
1062
+ ctx.jobs = runtime.jobs || {};
1063
+ ctx.getBreaker = (agentId) => {
1064
+ const agentCfg = runtime.agents?.[agentId];
1065
+ const modelId = agentCfg?.model || "unknown";
1066
+ return daemon.executor._getBreaker(modelId, runtime.runtime);
1067
+ };
1068
+ }
1069
+
1070
+ // Progress ping — fires once after 10s if turn is still running
1071
+ let progressPingFired = false;
1072
+ const progressPingTimer = setTimeout(async () => {
1073
+ if (!progressPingFired && typingToken && typingChatId) {
1074
+ progressPingFired = true;
1075
+ try {
1076
+ await fetch(`https://api.telegram.org/bot${typingToken}/sendMessage`, {
1077
+ method: "POST",
1078
+ headers: { "content-type": "application/json" },
1079
+ body: JSON.stringify({ chat_id: typingChatId, text: "Still working ⏳" }),
1080
+ });
1081
+ } catch (_) {}
1082
+ }
1083
+ }, 10_000);
1084
+
1085
+ // Turn timeout — 2 minutes hard cap
1086
+ const MAX_TURN_MS = 120_000;
1087
+ let turnTimeoutTimer;
1088
+ const turnTimeoutPromise = new Promise((_, reject) => {
1089
+ turnTimeoutTimer = setTimeout(() => reject(new Error("turn_timeout")), MAX_TURN_MS);
1090
+ });
1091
+
1092
+ let run;
1093
+ try {
1094
+ run = await Promise.race([
1095
+ daemon.executor.executeJob(job.job_id, {
1096
+ mode: executorMode,
1097
+ plan,
1098
+ shadowImport: false, // already done in hydrateInteractiveJob
1099
+ sessionId: job.chat_id || job.job_id,
1100
+ skipApprovalNotification: job.source === "tui",
1101
+ }),
1102
+ turnTimeoutPromise,
1103
+ ]);
1104
+ } finally {
1105
+ clearTimeout(progressPingTimer);
1106
+ clearTimeout(turnTimeoutTimer);
1107
+ if (typingTimer) clearInterval(typingTimer);
1108
+ }
1109
+
1110
+ // Extract response text from the run result
1111
+ const resultOutput = run?.result?.output;
1112
+ const responseText = typeof resultOutput === "string"
1113
+ ? resultOutput
1114
+ : resultOutput?.output || resultOutput?.summary || run?.result?.summary || "(no response)";
1115
+
1116
+ // Detect waiting_for_human: response ends with a question mark
1117
+ const responseStr = typeof responseText === "string" ? responseText : JSON.stringify(responseText);
1118
+ const endsWithQuestion = responseStr.trimEnd().endsWith("?");
1119
+ if (endsWithQuestion && stateStore.db) {
1120
+ stateStore.db.prepare("UPDATE interactive_jobs SET waiting_for_human = 1 WHERE job_id = ?").run(job.job_id);
1121
+ }
1122
+ const toolCalls = run?.result?.toolCalls || run?.toolCalls || [];
1123
+
1124
+ // Deliver response to source agent if this was an inter-agent message
1125
+ if (job.source_type === 'agent' && job.reply_session_key) {
1126
+ let agentDelivered = false;
1127
+ try {
1128
+ const { OpenClawPeerDeliveryAdapter } = await import("./runtime/delivery-adapters/openclaw-peer.js");
1129
+ const peerAdapter = new OpenClawPeerDeliveryAdapter();
1130
+ await peerAdapter.deliver(
1131
+ { message: typeof responseText === "string" ? responseText : JSON.stringify(responseText) },
1132
+ { target: { sessionKey: job.reply_session_key }, profileConfig: {} }
1133
+ );
1134
+ agentDelivered = true;
1135
+ console.log(JSON.stringify({ service: "inter_agent_routing", event: "delivered", jobId: job.job_id, replySessionKey: job.reply_session_key }));
1136
+ } catch (peerErr) {
1137
+ console.warn(JSON.stringify({ service: "inter_agent_routing", event: "delivery_failed", jobId: job.job_id, error: peerErr.message }));
1138
+ }
1139
+ if (!agentDelivered) {
1140
+ // Fallback: send to operator Telegram with warning prefix
1141
+ const telegramConfig = (await daemon.scheduler.loadRuntime()).runtime?.telegram;
1142
+ const botToken = telegramConfig?.botTokenEnv ? process.env[telegramConfig.botTokenEnv] : null;
1143
+ const operatorChatId = telegramConfig?.operatorChatId;
1144
+ if (botToken && operatorChatId) {
1145
+ const { splitTelegramMessage, markdownToTelegramHtml } = await import("./runtime/telegram-inbound.js");
1146
+ const fallbackText = `⚠️ [Agent reply — routing failed, sending here instead]\n\n${typeof responseText === "string" ? responseText : JSON.stringify(responseText)}`;
1147
+ const htmlText = markdownToTelegramHtml(fallbackText);
1148
+ for (const chunk of splitTelegramMessage(htmlText)) {
1149
+ await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
1150
+ method: "POST",
1151
+ headers: { "content-type": "application/json" },
1152
+ body: JSON.stringify({ chat_id: operatorChatId, text: chunk, parse_mode: "HTML" }),
1153
+ }).catch(() => {});
1154
+ }
1155
+ }
1156
+ }
1157
+ }
1158
+
1159
+ if (job.source === "tui" && job.chat_id && tuiDeliveryAdapter && tuiServer) {
1160
+ try {
1161
+ const turnTokens = (run?.usage?.tokensIn || 0) + (run?.usage?.tokensOut || 0);
1162
+ await tuiDeliveryAdapter.deliver({
1163
+ jobId: job.job_id,
1164
+ message: typeof responseText === "string" ? responseText : JSON.stringify(responseText),
1165
+ tokens: turnTokens,
1166
+ cost: run?.costUsd || 0,
1167
+ toolCalls,
1168
+ }, {
1169
+ sessionKey: job.chat_id,
1170
+ status: await tuiServer.buildStatusPayload(job.chat_id),
1171
+ });
1172
+ } catch (deliveryError) {
1173
+ console.error(JSON.stringify({ service: "tui_delivery", jobId: job.job_id, error: deliveryError.message }));
1174
+ }
1175
+ }
1176
+
1177
+ // Deliver response to Telegram (skip for agent-only dispatch jobs)
1178
+ const telegramConfig = (await daemon.scheduler.loadRuntime()).runtime?.telegram;
1179
+ const botToken = telegramConfig?.botTokenEnv ? process.env[telegramConfig.botTokenEnv] : null;
1180
+ const deliverChatId = job.source_type === 'agent'
1181
+ ? null // already handled above
1182
+ : (job.chat_id || (job.source === "agent_dispatch" ? telegramConfig?.operatorChatId : null));
1183
+ if (job.source === "telegram" && botToken && deliverChatId && job.source !== "completion_ping") {
1184
+ const { splitTelegramMessage, markdownToTelegramHtml } = await import("./runtime/telegram-inbound.js");
1185
+ // Strip XML tool call artifacts that some models emit inline in text
1186
+ // alongside native tool_use blocks. Covers multiple format variants:
1187
+ // <function_calls>...</function_calls> (Anthropic legacy)
1188
+ // <tool_call>...</tool_call> (Qwen / OpenAI compat)
1189
+ // <tool_response>...</tool_response> (model echoing tool results)
1190
+ // <invoke>...</invoke> (Claude XML compat)
1191
+ const cleanResponseText = (typeof responseText === "string" ? responseText : JSON.stringify(responseText))
1192
+ .replace(/<function_calls>[\s\S]*?<\/function_calls>/g, "")
1193
+ .replace(/<function_response>[\s\S]*?<\/function_response>/g, "")
1194
+ .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, "")
1195
+ .replace(/<tool_response>[\s\S]*?<\/tool_response>/g, "")
1196
+ .replace(/<invoke>[\s\S]*?<\/invoke>/g, "")
1197
+ .replace(/<function_calls>[\s\S]*?<\/antml:function_calls>/g, "")
1198
+ .trim();
1199
+ const htmlText = markdownToTelegramHtml(cleanResponseText || "(no response)");
1200
+ const chunks = splitTelegramMessage(htmlText);
1201
+ for (const chunk of chunks) {
1202
+ const sendRes = await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
1203
+ method: "POST",
1204
+ headers: { "content-type": "application/json" },
1205
+ body: JSON.stringify({ chat_id: deliverChatId, text: chunk, parse_mode: "HTML" }),
1206
+ });
1207
+ if (!sendRes.ok) {
1208
+ const sendBody = await sendRes.text().catch(() => "");
1209
+ console.error(JSON.stringify({ service: "interactive_delivery", jobId: job.job_id, chatId: deliverChatId, status: sendRes.status, error: sendBody }));
1210
+ }
1211
+ }
1212
+ }
1213
+
1214
+ // React ✅ on the original inbound message after Telegram delivery
1215
+ if (botToken && deliverChatId && job.source === "telegram" && job.job_id) {
1216
+ try {
1217
+ // jobId format: telegram-<chatId>-<messageId>
1218
+ const parts = job.job_id.split("-");
1219
+ const inboundMsgId = parts.length >= 3 ? Number(parts[parts.length - 1]) : null;
1220
+ if (inboundMsgId) {
1221
+ fetch(`https://api.telegram.org/bot${botToken}/setMessageReaction`, {
1222
+ method: "POST",
1223
+ headers: { "content-type": "application/json" },
1224
+ body: JSON.stringify({ chat_id: deliverChatId, message_id: inboundMsgId, reaction: [{ type: "emoji", emoji: "✅" }] }),
1225
+ }).catch(() => {}); // best-effort
1226
+ }
1227
+ } catch (_) {}
1228
+ }
1229
+
1230
+ // Deliver response to Slack
1231
+ if (slackHandler && job.source === "slack") {
1232
+ const channelId = job.chat_id.split(":")[2]; // slack:teamId:channelId
1233
+ const cleanResponseText = (typeof responseText === "string" ? responseText : JSON.stringify(responseText))
1234
+ .replace(/<function_calls>[\s\S]*?<\/function_calls>/g, "")
1235
+ .replace(/<function_response>[\s\S]*?<\/function_response>/g, "")
1236
+ .replace(/<tool_call>[\s\S]*?<\/tool_call>/g, "")
1237
+ .replace(/<tool_response>[\s\S]*?<\/tool_response>/g, "")
1238
+ .replace(/<invoke>[\s\S]*?<\/invoke>/g, "")
1239
+ .trim();
1240
+ await slackHandler.postMessage(channelId, cleanResponseText || "(no response)");
1241
+ }
1242
+
1243
+ // Update conversation context (blob — existing source of truth)
1244
+ if (chatSession) {
1245
+ let ctx = chatSession.conversation_context || null;
1246
+ const imageRef = job.image_refs
1247
+ ? JSON.parse(job.image_refs)?.[0] || null
1248
+ : null;
1249
+ ctx = appendTurn(ctx, "user", job.input, imageRef);
1250
+ ctx = appendTurn(ctx, "assistant", typeof responseText === "string" ? responseText : JSON.stringify(responseText));
1251
+ stateStore.updateConversationContext(job.chat_id, ctx);
1252
+ }
1253
+
1254
+ // Dual-write: context ledger events (Phase 1 — ledger is passive, blob drives behaviour)
1255
+ try {
1256
+ const sessionId = job.chat_id || job.job_id;
1257
+ const msgInEvent = contextLedger.appendEvent({
1258
+ session_id: sessionId,
1259
+ kind: "message_in",
1260
+ payload_json: { channel: job.source || "telegram", user_id: job.chat_id || "", text: job.input },
1261
+ });
1262
+
1263
+ // Log tool calls/results if present
1264
+ for (const tc of toolCalls) {
1265
+ const tcEvent = contextLedger.appendEvent({
1266
+ session_id: sessionId,
1267
+ kind: "tool_call",
1268
+ payload_json: { tool_name: tc.name || tc.tool_name, call_id: tc.id || tc.call_id || "", args: tc.args || tc.input },
1269
+ parent_event_id: msgInEvent.id,
1270
+ });
1271
+ if (tc.result !== undefined || tc.output !== undefined) {
1272
+ contextLedger.appendEvent({
1273
+ session_id: sessionId,
1274
+ kind: "tool_result",
1275
+ payload_json: { tool_name: tc.name || tc.tool_name, call_id: tc.id || tc.call_id || "", ok: !tc.error, result: tc.result ?? tc.output },
1276
+ parent_event_id: tcEvent.id,
1277
+ });
1278
+ }
1279
+ }
1280
+
1281
+ contextLedger.appendEvent({
1282
+ session_id: sessionId,
1283
+ kind: "message_out",
1284
+ payload_json: { channel: job.source || "telegram", text: typeof responseText === "string" ? responseText : JSON.stringify(responseText) },
1285
+ token_cost_in: run?.usage?.tokensIn ?? run?.usage?.prompt_tokens ?? run?.result?.usage?.prompt_tokens ?? null,
1286
+ token_cost_out: run?.usage?.tokensOut ?? run?.usage?.completion_tokens ?? run?.result?.usage?.completion_tokens ?? null,
1287
+ parent_event_id: msgInEvent.id,
1288
+ });
1289
+
1290
+ // Emit outbox item for Telegram sends (passive — not enforced yet)
1291
+ if (job.chat_id && job.source === "telegram" && job.source !== "completion_ping") {
1292
+ contextLedger.enqueueOutbox({
1293
+ session_id: sessionId,
1294
+ kind: "send_telegram",
1295
+ payload_json: { chat_id: job.chat_id, text: typeof responseText === "string" ? responseText : JSON.stringify(responseText) },
1296
+ dedupe_key: `send_telegram:${job.job_id}`,
1297
+ });
1298
+ }
1299
+ } catch (ledgerErr) {
1300
+ // Dual-write: ledger failure must not break the turn
1301
+ console.error(JSON.stringify({ service: "context_ledger", error: ledgerErr.message, jobId: job.job_id }));
1302
+ }
1303
+
1304
+ return run;
1305
+ }, {
1306
+ onJobFailure: async (job, error) => {
1307
+ if (job.source === "tui" && job.chat_id && tuiDeliveryAdapter && tuiServer) {
1308
+ try {
1309
+ await tuiDeliveryAdapter.deliver({
1310
+ jobId: job.job_id,
1311
+ error: error.message,
1312
+ }, {
1313
+ sessionKey: job.chat_id,
1314
+ status: await tuiServer.buildStatusPayload(job.chat_id),
1315
+ });
1316
+ } catch (deliveryError) {
1317
+ console.error(JSON.stringify({ service: "tui_delivery", jobId: job.job_id, error: deliveryError.message }));
1318
+ }
1319
+ return;
1320
+ }
1321
+ if (job.source !== "telegram") return;
1322
+ const tgCfg = (await daemon.scheduler.loadRuntime()).runtime?.telegram;
1323
+ const botToken = tgCfg?.botTokenEnv ? process.env[tgCfg.botTokenEnv] : null;
1324
+ await sendFailurePing(job, error, { botToken, operatorChatId: tgCfg?.operatorChatId });
1325
+ },
1326
+ });
1327
+ await processInteractiveHandoffs(stateStore);
1328
+
1329
+ // Fire completion pings for finished dispatch jobs
1330
+ const tgConf = (await daemon.scheduler.loadRuntime()).runtime?.telegram;
1331
+ const pingToken = tgConf?.botTokenEnv ? process.env[tgConf.botTokenEnv] : null;
1332
+ if (tgConf && pingToken) {
1333
+ await fireCompletionPings(stateStore, tgConf, { botToken: pingToken });
1334
+ }
1335
+ } catch (error) {
1336
+ console.error(JSON.stringify({ service: "interactive_drain", error: error.message }));
1337
+ }
1338
+ await new Promise((resolve) => setTimeout(resolve, 100));
1339
+ }
1340
+ };
1341
+
1342
+ const drainPromise = interactiveDrain();
1343
+
1344
+ const shutdown = async () => {
1345
+ if (shuttingDown) return;
1346
+ shuttingDown = true;
1347
+ interactiveRunning = false;
1348
+ if (telegramPoller) telegramPoller.stop();
1349
+ if (timer) {
1350
+ clearInterval(timer);
1351
+ timer = null;
1352
+ }
1353
+ // Wait for in-flight interactive jobs to complete before exiting
1354
+ await waitForDrainEmpty(drainTimeoutMs);
1355
+ await drainPromise;
1356
+ if (tuiServer) {
1357
+ await tuiServer.close();
1358
+ }
1359
+ contextLedger.close();
1360
+ await daemon.close({ drainTimeoutMs });
1361
+ removePidFile();
1362
+ process.exit(0);
1363
+ };
1364
+
1365
+ process.once("SIGINT", shutdown);
1366
+ process.once("SIGTERM", shutdown);
1367
+
1368
+ console.log(
1369
+ JSON.stringify({
1370
+ ok: true,
1371
+ service: "scheduler_daemon",
1372
+ mode,
1373
+ intervalMs: pollIntervalMs,
1374
+ maxJobsPerTick: runtime.runtime?.concurrency?.maxJobsPerTick ?? runtime.runtime?.concurrency?.maxConcurrentJobs ?? 2
1375
+ })
1376
+ );
1377
+
1378
+ await runTick();
1379
+ timer = setInterval(() => {
1380
+ runTick();
1381
+ }, pollIntervalMs);
1382
+ }
1383
+
1384
+ async function pruneState(scope = "all") {
1385
+ const runtime = await scheduler.loadRuntime();
1386
+ const statePath = path.join(projectRoot, "state");
1387
+ const operations = [];
1388
+
1389
+ if (scope === "all" || scope === "runs") {
1390
+ operations.push(
1391
+ new RunStore({
1392
+ rootDir: path.join(statePath, "runs"),
1393
+ retention: runtime.runtime?.retention?.runs || {}
1394
+ }).prune()
1395
+ );
1396
+ }
1397
+ if (scope === "all" || scope === "notifications") {
1398
+ operations.push(
1399
+ new NotificationStore({
1400
+ rootDir: path.join(statePath, "notifications"),
1401
+ retention: runtime.runtime?.retention?.notifications || {}
1402
+ }).prune()
1403
+ );
1404
+ }
1405
+ if (scope === "all" || scope === "deliveries") {
1406
+ operations.push(
1407
+ new DeliveryStore({
1408
+ rootDir: path.join(statePath, "deliveries"),
1409
+ retention: runtime.runtime?.retention?.deliveries || {}
1410
+ }).prune()
1411
+ );
1412
+ }
1413
+ if (scope === "all" || scope === "transport") {
1414
+ operations.push(
1415
+ pruneJsonBuckets(
1416
+ path.join(statePath, "transport", "inbox"),
1417
+ buildRetentionPolicy(runtime.runtime?.retention?.transportInbox || {}, {
1418
+ ttlDays: 7,
1419
+ maxFilesPerBucket: 1000
1420
+ })
1421
+ )
1422
+ );
1423
+ }
1424
+
1425
+ const results = await Promise.all(operations);
1426
+ console.log(JSON.stringify({ scope, results }, null, 2));
1427
+ }
1428
+
1429
+ async function reviewInbox(limit = "10") {
1430
+ const server = new StandaloneTransportServer({
1431
+ stateRoot: path.join(projectRoot, "state")
1432
+ });
1433
+ const items = await server.listInbox(Number(limit) || 10);
1434
+ console.log(JSON.stringify(items, null, 2));
1435
+ }
1436
+
1437
+ async function evaluateRuns(limit = 20) {
1438
+ const evaluation = await evaluator.evaluate(Number(limit));
1439
+ console.log(JSON.stringify(evaluation, null, 2));
1440
+ }
1441
+
1442
+ async function evaluateJob(jobId) {
1443
+ const evaluation = await evaluator.evaluateAndPersistJob(jobId);
1444
+ console.log(JSON.stringify(evaluation, null, 2));
1445
+ }
1446
+
1447
+ async function improvementTargets() {
1448
+ const targets = await improvementHarness.listTargets();
1449
+ console.log(JSON.stringify(targets, null, 2));
1450
+ }
1451
+
1452
+ async function runImprovement(targetId, variantId = "baseline") {
1453
+ const result = await improvementHarness.runVariant(targetId, variantId);
1454
+ console.log(JSON.stringify(result, null, 2));
1455
+ }
1456
+
1457
+ async function runImprovementWithReportFallback(targetId, variantId = "baseline") {
1458
+ const result = await improvementHarness.runVariant(targetId, variantId, {
1459
+ allowReportFallback: true
1460
+ });
1461
+ console.log(JSON.stringify(result, null, 2));
1462
+ }
1463
+
1464
+ async function compareImprovements(targetId, candidateVariantId, baselineVariantId = "baseline") {
1465
+ const result = await improvementHarness.compareVariants(targetId, baselineVariantId, candidateVariantId);
1466
+ console.log(JSON.stringify(result, null, 2));
1467
+ }
1468
+
1469
+ async function compareImprovementsWithReportFallback(targetId, candidateVariantId, baselineVariantId = "baseline") {
1470
+ const result = await improvementHarness.compareVariants(targetId, baselineVariantId, candidateVariantId, {
1471
+ allowReportFallback: true
1472
+ });
1473
+ console.log(JSON.stringify(result, null, 2));
1474
+ }
1475
+
1476
+ async function repairAndCompareImprovement(targetId, candidateVariantId, baselineVariantId = "baseline") {
1477
+ const result = await improvementHarness.repairAndCompare(targetId, candidateVariantId, baselineVariantId);
1478
+ console.log(JSON.stringify(result, null, 2));
1479
+ }
1480
+
1481
+ async function repairAndCompareImprovementWithReportFallback(targetId, candidateVariantId, baselineVariantId = "baseline") {
1482
+ const result = await improvementHarness.repairAndCompare(targetId, candidateVariantId, baselineVariantId, {
1483
+ allowReportFallback: true
1484
+ });
1485
+ console.log(JSON.stringify(result, null, 2));
1486
+ }
1487
+
1488
+ async function shadowEvalJob(jobId, modelOverride = null) {
1489
+ const run = await executor.executeJob(jobId, {
1490
+ mode: "provider",
1491
+ shadowImport: true,
1492
+ modelOverride
1493
+ });
1494
+ const evaluation = await evaluator.evaluateAndPersistJob(jobId);
1495
+ console.log(
1496
+ JSON.stringify(
1497
+ {
1498
+ run: {
1499
+ filePath: run.filePath,
1500
+ mode: run.mode,
1501
+ providerId: run.providerId,
1502
+ modelId: run.modelId,
1503
+ routingDecision: run.routingDecision,
1504
+ preflight: run.preflight,
1505
+ summary: run.result.summary
1506
+ },
1507
+ evaluation
1508
+ },
1509
+ null,
1510
+ 2
1511
+ )
1512
+ );
1513
+ }
1514
+
1515
+ async function benchmarkModels(jobId, models) {
1516
+ if (!models.length) {
1517
+ throw new Error("benchmark-models requires at least one model id");
1518
+ }
1519
+
1520
+ const results = [];
1521
+ for (const modelId of models) {
1522
+ const run = await executor.executeJob(jobId, {
1523
+ mode: "provider",
1524
+ shadowImport: true,
1525
+ modelOverride: modelId
1526
+ });
1527
+ const evaluation = await evaluator.evaluateAndPersistJob(jobId);
1528
+ results.push({
1529
+ modelId,
1530
+ runFile: run.filePath,
1531
+ evalFile: evaluation.filePath,
1532
+ overallScore: evaluation.rubric.overallScore,
1533
+ contractAdherence: evaluation.contractCheck?.satisfiedRatio ?? null,
1534
+ comparisonNotes: evaluation.comparisonNotes
1535
+ });
1536
+ }
1537
+
1538
+ console.log(JSON.stringify({ jobId, results }, null, 2));
1539
+ }
1540
+
1541
+ async function indexEmbeddings(agentId) {
1542
+ const result = await scheduler.memoryStore.rebuildEmbeddings(agentId, {
1543
+ embeddingIndex
1544
+ });
1545
+ console.log(JSON.stringify(result, null, 2));
1546
+ }
1547
+
1548
+ async function queryEmbeddings(agentId, query) {
1549
+ const results = await embeddingIndex.query(agentId, query, 10);
1550
+ console.log(JSON.stringify(results, null, 2));
1551
+ }
1552
+
1553
+ async function embeddingHealth(agentId, probe = false) {
1554
+ const result = await scheduler.memoryStore.getEmbeddingHealth(agentId, {
1555
+ embeddingIndex,
1556
+ probe
1557
+ });
1558
+ console.log(JSON.stringify(result, null, 2));
1559
+ }
1560
+
1561
+ async function embeddingReadiness() {
1562
+ const result = await embeddingService.getReadiness();
1563
+ console.log(JSON.stringify(result, null, 2));
1564
+ }
1565
+
1566
+ async function reportFallbackReadinessData(jobId = "workspace-health") {
1567
+ const runtime = await scheduler.loadRuntime();
1568
+ const job = runtime.jobs[jobId];
1569
+ if (!job) {
1570
+ throw new Error(`Unknown job: ${jobId}`);
1571
+ }
1572
+
1573
+ const policy = getReportFallbackPolicy(runtime.runtime);
1574
+ const provider = runtime.providers.openrouter || null;
1575
+ const authRef = provider?.authRef || null;
1576
+ const envName = authRef?.startsWith("env:") ? authRef.slice(4) : null;
1577
+ const authPresent = envName ? Boolean(process.env[envName]) : false;
1578
+
1579
+ return {
1580
+ jobId,
1581
+ jobConfigured: Boolean(job.reportFallback?.enabled),
1582
+ jobPolicy: job.reportFallback || null,
1583
+ runtimePolicy: policy,
1584
+ remoteProviderModeAllowed: Boolean(process.env.NEMORIS_ALLOW_REMOTE_PROVIDER_MODE === "1" || process.env.NEMORIS_ALLOW_REMOTE_PROVIDER_MODE === "true"),
1585
+ providerConfigured: Boolean(provider),
1586
+ providerId: provider?.id || null,
1587
+ authRef,
1588
+ authPresent,
1589
+ ready: Boolean(job.reportFallback?.enabled && policy.enabled && authPresent && (process.env.NEMORIS_ALLOW_REMOTE_PROVIDER_MODE === "1" || process.env.NEMORIS_ALLOW_REMOTE_PROVIDER_MODE === "true")),
1590
+ blockedReasons: [
1591
+ !job.reportFallback?.enabled ? "job_not_configured" : null,
1592
+ !policy.enabled ? "report_fallback_disabled" : null,
1593
+ !provider ? "openrouter_provider_missing" : null,
1594
+ provider && !authPresent ? "openrouter_auth_missing" : null,
1595
+ !(process.env.NEMORIS_ALLOW_REMOTE_PROVIDER_MODE === "1" || process.env.NEMORIS_ALLOW_REMOTE_PROVIDER_MODE === "true")
1596
+ ? "remote_provider_mode_disabled"
1597
+ : null
1598
+ ].filter(Boolean)
1599
+ };
1600
+ }
1601
+
1602
+ async function reportFallbackReadiness(jobId = "workspace-health") {
1603
+ const result = await reportFallbackReadinessData(jobId);
1604
+ console.log(JSON.stringify(result, null, 2));
1605
+ }
1606
+
1607
+ async function configValidate() {
1608
+ const config = await scheduler.loadRuntime();
1609
+ const schemaResult = validateAllConfigs(config, path.join(projectRoot, "config"));
1610
+ console.log(
1611
+ JSON.stringify(
1612
+ {
1613
+ ok: schemaResult.ok,
1614
+ checkedAt: new Date().toISOString(),
1615
+ providers: Object.keys(config.providers),
1616
+ agents: Object.keys(config.agents),
1617
+ jobs: Object.keys(config.jobs).length,
1618
+ lanes: Object.keys(config.router).length,
1619
+ schemaErrors: schemaResult.errors
1620
+ },
1621
+ null,
1622
+ 2
1623
+ )
1624
+ );
1625
+ if (!schemaResult.ok) {
1626
+ process.exitCode = 1;
1627
+ }
1628
+ }
1629
+
1630
+ async function peerReadiness(peerId = "nemo") {
1631
+ const runtime = await scheduler.loadRuntime();
1632
+ const result = await peerReadinessProbe.probe(runtime, peerId);
1633
+ console.log(JSON.stringify(result, null, 2));
1634
+ }
1635
+
1636
+ async function dependencyHealthForJob(jobId = "workspace-health") {
1637
+ const result = await dependencyHealth.forJob(jobId);
1638
+ console.log(JSON.stringify(result, null, 2));
1639
+ }
1640
+
1641
+ async function getLaneReadiness(jobId = "workspace-health") {
1642
+ const runtime = await scheduler.loadRuntime();
1643
+ const job = runtime.jobs[jobId];
1644
+ if (!job) {
1645
+ throw new Error(`Unknown job: ${jobId}`);
1646
+ }
1647
+
1648
+ const [evaluation, maintenance, embeddingHealth] = await Promise.all([
1649
+ evaluator.evaluateJob(jobId),
1650
+ daemon.stateStore.getMeta("maintenance", null),
1651
+ scheduler.memoryStore.getEmbeddingHealth(job.agentId, {
1652
+ embeddingIndex,
1653
+ probe: false
1654
+ })
1655
+ ]);
1656
+
1657
+ const [reportFallback, dependencies] = await Promise.all([
1658
+ reportFallbackReadinessData(jobId),
1659
+ dependencyHealth.forJob(jobId)
1660
+ ]);
1661
+
1662
+ const readiness = buildLaneReadiness({
1663
+ jobId,
1664
+ job,
1665
+ evaluation,
1666
+ embeddingHealth,
1667
+ reportFallback,
1668
+ maintenance,
1669
+ dependencyHealth: dependencies
1670
+ });
1671
+
1672
+ return readiness;
1673
+ }
1674
+
1675
+ async function laneReadiness(jobId = "workspace-health") {
1676
+ const readiness = await getLaneReadiness(jobId);
1677
+
1678
+ console.log(JSON.stringify(readiness, null, 2));
1679
+ }
1680
+
1681
+ async function pilotStatus(jobId = "workspace-health", limit = 10) {
1682
+ const [readiness, review, evaluation, dependencies, reportFallback] = await Promise.all([
1683
+ getLaneReadiness(jobId),
1684
+ reviewer.review(Number(limit)),
1685
+ evaluator.evaluateJob(jobId),
1686
+ dependencyHealth.forJob(jobId),
1687
+ reportFallbackReadinessData(jobId)
1688
+ ]);
1689
+
1690
+ const status = buildPilotStatus({
1691
+ jobId,
1692
+ readiness,
1693
+ evaluation,
1694
+ runs: review.recentRuns || [],
1695
+ dependencyHealth: dependencies,
1696
+ reportFallback
1697
+ });
1698
+
1699
+ console.log(JSON.stringify(status, null, 2));
1700
+ }
1701
+
1702
+ async function cutoverReadiness(jobId = "workspace-health", limit = 10) {
1703
+ const [readiness, review, evaluation, dependencies, reportFallback] = await Promise.all([
1704
+ getLaneReadiness(jobId),
1705
+ reviewer.review(Number(limit)),
1706
+ evaluator.evaluateJob(jobId),
1707
+ dependencyHealth.forJob(jobId),
1708
+ reportFallbackReadinessData(jobId)
1709
+ ]);
1710
+
1711
+ const status = buildPilotStatus({
1712
+ jobId,
1713
+ readiness,
1714
+ evaluation,
1715
+ runs: review.recentRuns || [],
1716
+ dependencyHealth: dependencies,
1717
+ reportFallback
1718
+ });
1719
+
1720
+ const cutover = buildCutoverReadiness({ readiness, pilotStatus: status });
1721
+ console.log(JSON.stringify(cutover, null, 2));
1722
+ }
1723
+
1724
+ async function buildPilotBundle(jobId = "workspace-health", limit = 10) {
1725
+ const [readiness, review, evaluation, dependencies, reportFallback] = await Promise.all([
1726
+ getLaneReadiness(jobId),
1727
+ reviewer.review(Number(limit)),
1728
+ evaluator.evaluateJob(jobId),
1729
+ dependencyHealth.forJob(jobId),
1730
+ reportFallbackReadinessData(jobId)
1731
+ ]);
1732
+
1733
+ const status = buildPilotStatus({
1734
+ jobId,
1735
+ readiness,
1736
+ evaluation,
1737
+ runs: review.recentRuns || [],
1738
+ dependencyHealth: dependencies,
1739
+ reportFallback
1740
+ });
1741
+ const cutover = buildCutoverReadiness({ readiness, pilotStatus: status });
1742
+
1743
+ return {
1744
+ readiness,
1745
+ pilotStatus: status,
1746
+ cutover,
1747
+ evaluation
1748
+ };
1749
+ }
1750
+
1751
+ function summarizePilotBundle(bundle) {
1752
+ return {
1753
+ readiness: {
1754
+ tier: bundle.readiness.readinessTier,
1755
+ blockers: bundle.readiness.blockers,
1756
+ warnings: bundle.readiness.warnings,
1757
+ checks: bundle.readiness.checks,
1758
+ summary: bundle.readiness.summary,
1759
+ interactionDiagnosis: bundle.readiness.interaction?.diagnosis || null
1760
+ },
1761
+ pilotStatus: {
1762
+ recommendation: bundle.pilotStatus.recommendation,
1763
+ providerRunsObserved: bundle.pilotStatus.providerRunsObserved,
1764
+ healthyProviderRuns: bundle.pilotStatus.healthyProviderRuns,
1765
+ latestProviderRun: bundle.pilotStatus.latestProviderRun,
1766
+ reportFallback: {
1767
+ ready: bundle.pilotStatus.reportFallback?.ready ?? null,
1768
+ blockedReasons: bundle.pilotStatus.reportFallback?.blockedReasons || []
1769
+ }
1770
+ },
1771
+ cutover: bundle.cutover,
1772
+ evaluation: {
1773
+ overallScore: bundle.evaluation.rubric?.overallScore ?? null,
1774
+ contractSatisfiedRatio: bundle.evaluation.contractCheck?.satisfiedRatio ?? null,
1775
+ interactionDiagnosis: bundle.evaluation.interaction?.diagnosis || null,
1776
+ interactionArtifacts: bundle.evaluation.interactionArtifacts || null,
1777
+ comparisonNotes: bundle.evaluation.comparisonNotes || []
1778
+ }
1779
+ };
1780
+ }
1781
+
1782
+ async function dailyDriverTrustPack(limit = 10) {
1783
+ const workspaceHealth = await buildPilotBundle("workspace-health", limit);
1784
+ const memoryRollup = await buildPilotBundle("memory-rollup", limit);
1785
+
1786
+ const pack = {
1787
+ checkedAt: new Date().toISOString(),
1788
+ lanes: {
1789
+ workspaceHealth: summarizePilotBundle(workspaceHealth),
1790
+ memoryRollup: summarizePilotBundle(memoryRollup)
1791
+ },
1792
+ summary: {
1793
+ workspaceHealthVerdict: workspaceHealth.cutover.verdict,
1794
+ memoryRollupVerdict: memoryRollup.cutover.verdict,
1795
+ currentDailyDriver: workspaceHealth.cutover.verdict === "READY" ? "workspace-health" : null,
1796
+ nextLaneCandidate: "memory-rollup",
1797
+ rollbackTriggers: [
1798
+ "provider_unreachable",
1799
+ "delivery_uncertain",
1800
+ "expired_followups_or_handoffs",
1801
+ "contract_adherence_drop",
1802
+ "repeated_lexical_fallback"
1803
+ ]
1804
+ }
1805
+ };
1806
+
1807
+ console.log(JSON.stringify(pack, null, 2));
1808
+ }
1809
+
1810
+ async function jobEmbeddingHealth(jobId, probe = false) {
1811
+ const runtime = await scheduler.loadRuntime();
1812
+ const job = runtime.jobs[jobId];
1813
+ if (!job) {
1814
+ throw new Error(`Unknown job: ${jobId}`);
1815
+ }
1816
+ const agentId = job.agentId;
1817
+ const result = await scheduler.memoryStore.getEmbeddingHealth(agentId, {
1818
+ embeddingIndex,
1819
+ probe
1820
+ });
1821
+ console.log(
1822
+ JSON.stringify(
1823
+ {
1824
+ jobId,
1825
+ agentId,
1826
+ ...result
1827
+ },
1828
+ null,
1829
+ 2
1830
+ )
1831
+ );
1832
+ }
1833
+
1834
+ async function rebuildJobEmbeddings(jobId) {
1835
+ const plan = await scheduler.hydrateJob(jobId, { shadowImport: true });
1836
+ try {
1837
+ const result = await scheduler.memoryStore.rebuildEmbeddings(plan.agent.id, {
1838
+ embeddingIndex: scheduler.embeddingIndex
1839
+ });
1840
+ console.log(
1841
+ JSON.stringify(
1842
+ {
1843
+ jobId,
1844
+ agentId: plan.agent.id,
1845
+ workspaceRoot: plan.agent.workspaceRoot,
1846
+ shadowImport: true,
1847
+ ...result
1848
+ },
1849
+ null,
1850
+ 2
1851
+ )
1852
+ );
1853
+ } catch (error) {
1854
+ const health = await scheduler.memoryStore.getEmbeddingHealth(plan.agent.id, {
1855
+ embeddingIndex: scheduler.embeddingIndex,
1856
+ probe: false
1857
+ });
1858
+ console.log(
1859
+ JSON.stringify(
1860
+ {
1861
+ jobId,
1862
+ agentId: plan.agent.id,
1863
+ workspaceRoot: plan.agent.workspaceRoot,
1864
+ shadowImport: true,
1865
+ ok: false,
1866
+ error: error?.message || String(error),
1867
+ embeddingHealth: health.embeddingHealth
1868
+ },
1869
+ null,
1870
+ 2
1871
+ )
1872
+ );
1873
+ process.exitCode = 1;
1874
+ }
1875
+ }
1876
+
1877
+ async function memoryBackends(agentId) {
1878
+ const runtime = await scheduler.loadRuntime();
1879
+ const agent = runtime.agents[agentId];
1880
+ const result = await scheduler.backendManager.inspect(agentId, agent?.workspaceRoot || null);
1881
+ console.log(JSON.stringify(result, null, 2));
1882
+ }
1883
+
1884
+ async function listAgentCards() {
1885
+ const runtime = await scheduler.loadRuntime();
1886
+ const registry = new PeerRegistry(runtime.peers);
1887
+ console.log(JSON.stringify(registry.listCards(), null, 2));
1888
+ }
1889
+
1890
+ async function findAgentCards(query = "") {
1891
+ const runtime = await scheduler.loadRuntime();
1892
+ const registry = new PeerRegistry(runtime.peers);
1893
+ console.log(JSON.stringify(registry.findByCapability(query), null, 2));
1894
+ }
1895
+
1896
+ async function suggestPeers(taskType = "workspace_health", query = "") {
1897
+ const runtime = await scheduler.loadRuntime();
1898
+ const registry = new PeerRegistry(runtime.peers);
1899
+ const suggestions = registry.suggestPeers({
1900
+ taskType,
1901
+ query: query || taskType,
1902
+ preferredDeliveryProfile: runtime.delivery?.defaultInteractiveProfile || null,
1903
+ limit: 5
1904
+ });
1905
+ console.log(JSON.stringify(suggestions, null, 2));
1906
+ }
1907
+
1908
+ async function previewWorkspaceHealthValidation() {
1909
+ const run = await executor.executeJob("workspace-health", {
1910
+ mode: "dry-run",
1911
+ shadowImport: true
1912
+ });
1913
+ console.log(
1914
+ JSON.stringify(
1915
+ {
1916
+ jobId: "workspace-health",
1917
+ filePath: run.filePath,
1918
+ interaction: run.interaction,
1919
+ notificationFiles: run.notificationFiles || [],
1920
+ followUpFile: run.followUpFile || null,
1921
+ guidance: {
1922
+ previewDeliveryProfile: "gateway_preview_main",
1923
+ liveDeliveryProfile: "gateway_telegram_main"
1924
+ }
1925
+ },
1926
+ null,
1927
+ 2
1928
+ )
1929
+ );
1930
+ }
1931
+
1932
+ async function validateWorkspaceHealth(mode = "preview", modelOverride = null) {
1933
+ const runtimeMode = mode === "live" ? "live" : "shadow";
1934
+ const executionMode = mode === "live" ? "provider" : "dry-run";
1935
+ const run = await executor.executeJob("workspace-health", {
1936
+ mode: executionMode,
1937
+ shadowImport: true,
1938
+ modelOverride
1939
+ });
1940
+ const delivery = await deliveryManager.deliverPending({
1941
+ mode: runtimeMode,
1942
+ limit: 10,
1943
+ notificationFiles: run.notificationFiles || []
1944
+ });
1945
+ console.log(
1946
+ JSON.stringify(
1947
+ {
1948
+ validation: mode,
1949
+ run: {
1950
+ filePath: run.filePath,
1951
+ mode: run.mode,
1952
+ providerId: run.providerId,
1953
+ modelId: run.modelId,
1954
+ interaction: run.interaction,
1955
+ notificationFiles: run.notificationFiles || []
1956
+ },
1957
+ delivery
1958
+ },
1959
+ null,
1960
+ 2
1961
+ )
1962
+ );
1963
+ }
1964
+
1965
+ async function validateWorkspaceHealthFull(selection = "top", modelOverride = null, deliveryProfile = "peer_preview") {
1966
+ const run = await executor.executeJob("workspace-health", {
1967
+ mode: "dry-run",
1968
+ shadowImport: true,
1969
+ modelOverride
1970
+ });
1971
+ const pendingFollowUps = await deliveryManager.listPendingFollowUps(20);
1972
+ const followUp = pendingFollowUps.find((item) => (run.notificationFiles || []).includes(item.filePath));
1973
+ if (!followUp) {
1974
+ throw new Error("No pending follow-up was created for workspace-health.");
1975
+ }
1976
+ const consumed = await deliveryManager.consumeFollowUp(followUp.filePath);
1977
+ const handoff = consumed.created.find((item) => item.stage === "handoff");
1978
+ if (!handoff) {
1979
+ throw new Error("Consumed follow-up did not produce a handoff notification.");
1980
+ }
1981
+
1982
+ const chosen =
1983
+ selection === "top"
1984
+ ? await deliveryManager.chooseTopSuggestedPeer(handoff.filePath, { deliveryProfile })
1985
+ : await deliveryManager.chooseHandoffPeer(handoff.filePath, selection, { deliveryProfile });
1986
+
1987
+ const delivery = await deliveryManager.deliverPending({
1988
+ mode: "live",
1989
+ limit: 1,
1990
+ notificationFiles: [chosen.filePath]
1991
+ });
1992
+
1993
+ const evaluation = await evaluator.evaluateAndPersistJob("workspace-health");
1994
+ console.log(
1995
+ JSON.stringify(
1996
+ {
1997
+ run: {
1998
+ filePath: run.filePath,
1999
+ notificationFiles: run.notificationFiles || []
2000
+ },
2001
+ followUp: consumed.followUp,
2002
+ generatedNotifications: consumed.created,
2003
+ handoff: chosen,
2004
+ delivery,
2005
+ evaluation: {
2006
+ filePath: evaluation.filePath,
2007
+ interaction: evaluation.interaction,
2008
+ comparisonNotes: evaluation.comparisonNotes
2009
+ }
2010
+ },
2011
+ null,
2012
+ 2
2013
+ )
2014
+ );
2015
+ }
2016
+
2017
+ async function validateWorkspaceHealthRepeatable(selection = "top", modelOverride = null, deliveryProfile = "peer_preview") {
2018
+ const cleanup = await deliveryManager.sweepPendingFollowUps({
2019
+ now: new Date(),
2020
+ timeoutMinutes: 0,
2021
+ escalateOnExpiry: false
2022
+ });
2023
+ const run = await executor.executeJob("workspace-health", {
2024
+ mode: "dry-run",
2025
+ shadowImport: true,
2026
+ modelOverride
2027
+ });
2028
+ const pendingFollowUps = await deliveryManager.listPendingFollowUps(20);
2029
+ const followUp = pendingFollowUps.find((item) => (run.notificationFiles || []).includes(item.filePath));
2030
+ if (!followUp) {
2031
+ throw new Error("No pending follow-up was created for workspace-health.");
2032
+ }
2033
+ const consumed = await deliveryManager.consumeFollowUp(followUp.filePath);
2034
+ const handoff = consumed.created.find((item) => item.stage === "handoff");
2035
+ if (!handoff) {
2036
+ throw new Error("Consumed follow-up did not produce a handoff notification.");
2037
+ }
2038
+ const chosen =
2039
+ selection === "top"
2040
+ ? await deliveryManager.chooseTopSuggestedPeer(handoff.filePath, { deliveryProfile })
2041
+ : await deliveryManager.chooseHandoffPeer(handoff.filePath, selection, { deliveryProfile });
2042
+ const delivery = await deliveryManager.deliverPending({
2043
+ mode: "live",
2044
+ limit: 1,
2045
+ notificationFiles: [chosen.filePath]
2046
+ });
2047
+ const evaluation = await evaluator.evaluateAndPersistJob("workspace-health");
2048
+ console.log(
2049
+ JSON.stringify(
2050
+ {
2051
+ cleanup,
2052
+ run: {
2053
+ filePath: run.filePath,
2054
+ notificationFiles: run.notificationFiles || []
2055
+ },
2056
+ followUp: consumed.followUp,
2057
+ generatedNotifications: consumed.created,
2058
+ handoff: chosen,
2059
+ delivery,
2060
+ evaluation: {
2061
+ filePath: evaluation.filePath,
2062
+ interaction: evaluation.interaction,
2063
+ comparisonNotes: evaluation.comparisonNotes
2064
+ }
2065
+ },
2066
+ null,
2067
+ 2
2068
+ )
2069
+ );
2070
+ }
2071
+
2072
+ async function validateWorkspaceHealthPilot(
2073
+ selection = "top",
2074
+ mode = "dry-run",
2075
+ modelOverride = null,
2076
+ deliveryProfile = "peer_preview"
2077
+ ) {
2078
+ const cleanup = await deliveryManager.sweepPendingFollowUps({
2079
+ now: new Date(),
2080
+ timeoutMinutes: 0,
2081
+ escalateOnExpiry: false
2082
+ });
2083
+ const run = await executor.executeJob("workspace-health", {
2084
+ mode,
2085
+ shadowImport: true,
2086
+ modelOverride
2087
+ });
2088
+ const pendingFollowUps = await deliveryManager.listPendingFollowUps(20);
2089
+ const followUp = pendingFollowUps.find((item) => (run.notificationFiles || []).includes(item.filePath));
2090
+ if (!followUp) {
2091
+ throw new Error("No pending follow-up was created for workspace-health.");
2092
+ }
2093
+ const consumed = await deliveryManager.consumeFollowUp(followUp.filePath);
2094
+ const handoff = consumed.created.find((item) => item.stage === "handoff");
2095
+ if (!handoff) {
2096
+ throw new Error("Consumed follow-up did not produce a handoff notification.");
2097
+ }
2098
+ const chosen =
2099
+ selection === "top"
2100
+ ? await deliveryManager.chooseTopSuggestedPeer(handoff.filePath, { deliveryProfile })
2101
+ : await deliveryManager.chooseHandoffPeer(handoff.filePath, selection, { deliveryProfile });
2102
+ const delivery = await deliveryManager.deliverPending({
2103
+ mode: "live",
2104
+ limit: 1,
2105
+ notificationFiles: [chosen.filePath]
2106
+ });
2107
+ const evaluation = await evaluator.evaluateAndPersistJob("workspace-health");
2108
+ const readiness = await getLaneReadiness("workspace-health");
2109
+ console.log(
2110
+ JSON.stringify(
2111
+ {
2112
+ cleanup,
2113
+ run: {
2114
+ filePath: run.filePath,
2115
+ mode: run.mode,
2116
+ providerId: run.providerId,
2117
+ modelId: run.modelId,
2118
+ notificationFiles: run.notificationFiles || []
2119
+ },
2120
+ followUp: consumed.followUp,
2121
+ generatedNotifications: consumed.created,
2122
+ handoff: chosen,
2123
+ delivery,
2124
+ evaluation: {
2125
+ filePath: evaluation.filePath,
2126
+ interaction: evaluation.interaction,
2127
+ comparisonNotes: evaluation.comparisonNotes
2128
+ },
2129
+ readiness
2130
+ },
2131
+ null,
2132
+ 2
2133
+ )
2134
+ );
2135
+ }
2136
+
2137
+ async function validateMemoryRollup(mode = "provider", modelOverride = null, deliveryMode = "live") {
2138
+ const resolvedModelOverride = modelOverride && modelOverride !== "-" ? modelOverride : null;
2139
+ const run = await executor.executeJob("memory-rollup", {
2140
+ mode,
2141
+ shadowImport: true,
2142
+ modelOverride: resolvedModelOverride
2143
+ });
2144
+ const delivery = await deliveryManager.deliverPending({
2145
+ mode: deliveryMode,
2146
+ limit: 10,
2147
+ notificationFiles: run.notificationFiles || []
2148
+ });
2149
+ const evaluation = await evaluator.evaluateAndPersistJob("memory-rollup");
2150
+ const bundle = await buildPilotBundle("memory-rollup");
2151
+ const ackNotificationPresent = evaluation.interaction?.ackQueued === true;
2152
+ const completionNotificationPresent = evaluation.interaction?.completionQueued === true;
2153
+ const deliveryReceiptsPresent = evaluation.interaction?.deliveryEvidenceHealthy === true;
2154
+ const deliveryUncertain = evaluation.interaction?.deliveryUncertain === true;
2155
+
2156
+ console.log(
2157
+ JSON.stringify(
2158
+ {
2159
+ validation: {
2160
+ mode,
2161
+ deliveryMode,
2162
+ ok: ackNotificationPresent && completionNotificationPresent && deliveryReceiptsPresent && !deliveryUncertain,
2163
+ ackNotificationPresent,
2164
+ completionNotificationPresent,
2165
+ deliveryReceiptsPresent,
2166
+ deliveryUncertain
2167
+ },
2168
+ run: {
2169
+ filePath: run.filePath,
2170
+ mode: run.mode,
2171
+ providerId: run.providerId,
2172
+ modelId: run.modelId,
2173
+ notificationFiles: run.notificationFiles || []
2174
+ },
2175
+ delivery,
2176
+ evaluation: {
2177
+ filePath: evaluation.filePath,
2178
+ contractCheck: evaluation.contractCheck,
2179
+ interaction: evaluation.interaction,
2180
+ interactionArtifacts: evaluation.interactionArtifacts,
2181
+ comparisonNotes: evaluation.comparisonNotes
2182
+ },
2183
+ readiness: bundle.readiness,
2184
+ pilotStatus: bundle.pilotStatus,
2185
+ cutover: bundle.cutover
2186
+ },
2187
+ null,
2188
+ 2
2189
+ )
2190
+ );
2191
+ }
2192
+
2193
+ async function reviewPendingFollowUps(limit = 10) {
2194
+ const items = await deliveryManager.listPendingFollowUps(Number(limit));
2195
+ console.log(JSON.stringify(items, null, 2));
2196
+ }
2197
+
2198
+ async function sweepPendingFollowUps() {
2199
+ const result = await deliveryManager.sweepPendingFollowUps();
2200
+ console.log(JSON.stringify(result, null, 2));
2201
+ }
2202
+
2203
+ async function consumeFollowUp(notificationFilePath) {
2204
+ if (!notificationFilePath) {
2205
+ throw new Error("consume-follow-up requires <notification-file-path>");
2206
+ }
2207
+ const result = await deliveryManager.consumeFollowUp(notificationFilePath);
2208
+ console.log(JSON.stringify(result, null, 2));
2209
+ }
2210
+
2211
+ async function queryQmd(agentId, query) {
2212
+ const runtime = await scheduler.loadRuntime();
2213
+ const agent = runtime.agents[agentId];
2214
+ const result = await scheduler.backendManager.queryQmd(agentId, query, agent?.workspaceRoot || null, 5);
2215
+ console.log(JSON.stringify(result, null, 2));
2216
+ }
2217
+
2218
+ async function sqliteStatus(agentId) {
2219
+ const memoryStore = new MemoryStore({ rootDir: stateRoot });
2220
+ const result = await memoryStore.sqliteStatus(agentId);
2221
+ console.log(JSON.stringify(result, null, 2));
2222
+ }
2223
+
2224
+ async function sqliteCheckpoint(agentId, thresholdBytes) {
2225
+ const memoryStore = new MemoryStore({ rootDir: stateRoot });
2226
+ const result = await memoryStore.manageSqlite(agentId, {
2227
+ thresholdBytes: thresholdBytes ? Number(thresholdBytes) : undefined
2228
+ });
2229
+ console.log(JSON.stringify(result, null, 2));
2230
+ }
2231
+
2232
+ async function runtimeStatus(jsonFlag = false) {
2233
+ const installDir = resolveRuntimeInstallDir();
2234
+ const status = await buildRuntimeStatus({
2235
+ projectRoot: installDir,
2236
+ stateRoot: path.join(installDir, "state"),
2237
+ liveRoot: resolveLiveRoot(installDir)
2238
+ });
2239
+
2240
+ if (jsonFlag) {
2241
+ console.log(JSON.stringify(status, null, 2));
2242
+ } else {
2243
+ console.log(formatRuntimeStatus(status));
2244
+ }
2245
+ }
2246
+
2247
+ async function mcpList() {
2248
+ const { ConfigLoader } = await import("./config/loader.js");
2249
+ const loader = new ConfigLoader({ rootDir: path.join(projectRoot, "config") });
2250
+ const mcp = await loader.loadMcp();
2251
+
2252
+ if (!mcp.servers || Object.keys(mcp.servers).length === 0) {
2253
+ console.log(MCP_EMPTY_STATE_MESSAGE);
2254
+ return;
2255
+ }
2256
+
2257
+ const { McpConsumer } = await import("./mcp/consumer.js");
2258
+ const consumer = new McpConsumer({ config: mcp });
2259
+
2260
+ try {
2261
+ const schemas = await consumer.allSchemas();
2262
+ if (schemas.length === 0) {
2263
+ console.log("MCP servers configured but no tools available.");
2264
+ return;
2265
+ }
2266
+ console.log(`\nMCP Tools (${schemas.length}):\n`);
2267
+ let lastServer = null;
2268
+ for (const s of schemas) {
2269
+ if (s.server !== lastServer) {
2270
+ if (lastServer !== null) console.log("");
2271
+ console.log(` ${s.server}:`);
2272
+ lastServer = s.server;
2273
+ }
2274
+ console.log(` ${s.name.padEnd(40)} ${s.description}`);
2275
+ }
2276
+ console.log("");
2277
+ } finally {
2278
+ await consumer.shutdown();
2279
+ }
2280
+ }
2281
+
2282
+ async function listTools() {
2283
+ const { ToolRegistry } = await import("./tools/registry.js");
2284
+ const { registerMicroToolHandlers } = await import("./tools/micro/index.js");
2285
+ const registry = new ToolRegistry();
2286
+ registerMicroToolHandlers(registry, { workspaceRoot: projectRoot });
2287
+ try {
2288
+ const toolsDir = path.join(projectRoot, "config", "tools");
2289
+ await registry.loadTools(toolsDir);
2290
+ } catch { /* no tools dir is fine */ }
2291
+ console.log("Configured tools:\n");
2292
+ for (const id of registry.allToolIds()) {
2293
+ const tool = registry.resolve(id);
2294
+ console.log(` ${id.padEnd(20)} ${(tool.tier || "micro").padEnd(8)} ${tool.description || ""}`);
2295
+ }
2296
+ }
2297
+
2298
+ async function addTool(toolId) {
2299
+ if (!toolId) { console.error("Usage: nemoris tools add <tool-id>"); process.exitCode = 1; return; }
2300
+ const { readFile: readFileAsync, writeFile: writeFileAsync, mkdir: mkdirAsync } = await import("node:fs/promises");
2301
+ const catalogUrl = new URL("./tools/catalog.json", import.meta.url);
2302
+ const catalog = JSON.parse(await readFileAsync(catalogUrl, "utf8"));
2303
+ const entry = catalog[toolId];
2304
+ if (!entry) {
2305
+ console.error(`Unknown tool: ${toolId}. Available: ${Object.keys(catalog).join(", ")}`);
2306
+ process.exitCode = 1;
2307
+ return;
2308
+ }
2309
+ const toolsDir = path.join(projectRoot, "config", "tools");
2310
+ await mkdirAsync(toolsDir, { recursive: true });
2311
+ const lines = [
2312
+ `[tool]`,
2313
+ `id = "${toolId}"`,
2314
+ `tier = "${entry.tier}"`,
2315
+ `description = "${entry.description}"`,
2316
+ `command = "${entry.command}"`,
2317
+ `args = [${entry.args.map(a => `"${a}"`).join(", ")}]`,
2318
+ ];
2319
+ if (entry.env && Object.keys(entry.env).length > 0) {
2320
+ const envPairs = Object.entries(entry.env).map(([k, v]) => `${k} = "${v}"`).join(", ");
2321
+ lines.push(`env = { ${envPairs} }`);
2322
+ }
2323
+ lines.push("", `[tool.limits]`);
2324
+ lines.push(`timeout_ms = ${entry.tier === "cli" ? 120000 : 30000}`);
2325
+ lines.push(`max_output_bytes = ${entry.tier === "cli" ? 10485760 : 1048576}`);
2326
+ const toml = lines.join("\n") + "\n";
2327
+ await writeFileAsync(path.join(toolsDir, `${toolId}.toml`), toml);
2328
+ console.log(`✓ Tool '${toolId}' added to config/tools/${toolId}.toml`);
2329
+ for (const [envKey, prompt] of Object.entries(entry.env_prompts || {})) {
2330
+ if (!process.env[envKey]) {
2331
+ console.log(` Set ${envKey} in your environment (${prompt})`);
2332
+ }
2333
+ }
2334
+ }
2335
+
2336
+ async function listSkills() {
2337
+ const { SkillsLoader } = await import("./skills/loader.js");
2338
+ const loader = new SkillsLoader();
2339
+ const skillsDir = path.join(projectRoot, "config", "skills");
2340
+ await loader.loadSkills(skillsDir);
2341
+ const all = loader.listAll();
2342
+ if (all.length === 0) {
2343
+ console.log("No skills configured. Run 'nemoris setup' to create defaults.");
2344
+ return;
2345
+ }
2346
+ console.log("Configured skills:\n");
2347
+ for (const [id, skill] of all) {
2348
+ console.log(` ${id.padEnd(25)} agents: ${skill.agent_scope.join(", ")}`);
2349
+ console.log(` ${"".padEnd(25)} tools: ${[...skill.tools.required, ...skill.tools.optional].join(", ")}`);
2350
+ }
2351
+ }
2352
+
2353
+ async function listImprovements() {
2354
+ const { readdir: readdirAsync } = await import("node:fs/promises");
2355
+ const { ImprovementEngine } = await import("./runtime/improvement-engine.js");
2356
+ const engine = new ImprovementEngine({ stateRoot: path.join(projectRoot, "state") });
2357
+ const tuningsRoot = path.join(projectRoot, "state", "tunings");
2358
+ try {
2359
+ const jobDirs = await readdirAsync(tuningsRoot);
2360
+ let found = false;
2361
+ for (const jobId of jobDirs) {
2362
+ const tunings = await engine.loadTunings(jobId);
2363
+ if (tunings.length > 0) {
2364
+ found = true;
2365
+ console.log(`\n${jobId} (${tunings.length} active tunings):`);
2366
+ for (const t of tunings) {
2367
+ console.log(` ${t.trigger || "unknown"} — ${t.timestamp || "no timestamp"}`);
2368
+ }
2369
+ }
2370
+ }
2371
+ if (!found) console.log("No active tunings.");
2372
+ } catch {
2373
+ console.log("No active tunings.");
2374
+ }
2375
+ }
2376
+
2377
+ function writeOutput(stream, text) {
2378
+ if (!text) {
2379
+ return;
2380
+ }
2381
+ stream.write(text.endsWith("\n") ? text : `${text}\n`);
2382
+ }
2383
+
2384
+ export async function main(argv = process.argv) {
2385
+ let { command, rest } = parseArgs(argv);
2386
+
2387
+ if (!command) {
2388
+ writeOutput(process.stdout, formatGeneralHelp());
2389
+ return 0;
2390
+ }
2391
+
2392
+ if (command === "--help" || command === "-h") {
2393
+ writeOutput(process.stdout, formatGeneralHelp({ includeInternal: shouldShowAllCommands(rest) }));
2394
+ return 0;
2395
+ }
2396
+
2397
+ if (command === "--all" || command === "internal") {
2398
+ writeOutput(process.stdout, formatGeneralHelp({ includeInternal: true }));
2399
+ return 0;
2400
+ }
2401
+
2402
+ if (hasHelpFlag(rest)) {
2403
+ const helpText = formatCommandHelp(command);
2404
+ if (helpText) {
2405
+ writeOutput(process.stdout, helpText);
2406
+ return 0;
2407
+ }
2408
+ }
2409
+
2410
+ if (!isKnownCommand(command)) {
2411
+ writeOutput(process.stderr, formatGeneralHelp({ includeInternal: shouldShowAllCommands(rest) }));
2412
+ process.exitCode = 1;
2413
+ return 1;
2414
+ }
2415
+
2416
+ const aliasResolved = resolveAlias(command, rest);
2417
+ if (aliasResolved !== null) {
2418
+ [command, ...rest] = aliasResolved;
2419
+ }
2420
+
2421
+ if (command === "start") {
2422
+ const installDir = resolveRuntimeInstallDir();
2423
+ if (rest.includes("--foreground")) {
2424
+ const [mode = "live", intervalMs = "30000"] = rest.filter((arg) => arg !== "--foreground");
2425
+ await serveDaemon(mode, intervalMs, installDir);
2426
+ return 0;
2427
+ }
2428
+ const result = await startDaemonCommand({ projectRoot: installDir });
2429
+ writeOutput(result.exitCode === 0 ? process.stdout : process.stderr, result.message);
2430
+ process.exitCode = result.exitCode;
2431
+ return result.exitCode;
2432
+ } else if (command === "stop") {
2433
+ const result = await stopDaemonCommand({ projectRoot: resolveRuntimeInstallDir() });
2434
+ writeOutput(result.exitCode === 0 ? process.stdout : process.stderr, result.message);
2435
+ process.exitCode = result.exitCode;
2436
+ return result.exitCode;
2437
+ } else if (command === "restart") {
2438
+ const result = await restartDaemonCommand({ projectRoot: resolveRuntimeInstallDir() });
2439
+ writeOutput(result.exitCode === 0 ? process.stdout : process.stderr, result.message);
2440
+ process.exitCode = result.exitCode;
2441
+ return result.exitCode;
2442
+ } else if (command === "logs") {
2443
+ const result = await runLogsCommand({ projectRoot: resolveRuntimeInstallDir(), args: rest });
2444
+ writeOutput(result.stream || process.stdout, result.message || "");
2445
+ process.exitCode = result.exitCode;
2446
+ return result.exitCode;
2447
+ } else if (command === "chat") {
2448
+ const installDir = resolveRuntimeInstallDir();
2449
+ const { pidFile } = getDaemonPaths(installDir);
2450
+ const trackedPid = readTrackedPid(pidFile);
2451
+ const hasTrackedDaemon = isPidRunning(trackedPid);
2452
+ const runningPids = hasTrackedDaemon ? [trackedPid] : await listServeDaemonPids();
2453
+ if (runningPids.length === 0) {
2454
+ writeOutput(process.stderr, "Daemon not running. Start with: nemoris start");
2455
+ process.exitCode = 1;
2456
+ return 1;
2457
+ }
2458
+ const { parseChatFlags, runChat } = await import("./tui/chat.js");
2459
+ const flags = parseChatFlags(rest);
2460
+ return runChat({
2461
+ projectRoot: installDir,
2462
+ stateDir: path.join(installDir, "state"),
2463
+ agent: flags.agent,
2464
+ model: flags.model,
2465
+ noColor: flags.noColor,
2466
+ json: flags.json,
2467
+ });
2468
+ } else if (command === "demo") {
2469
+ await runDemo();
2470
+ } else if (command === "inspect-memory") {
2471
+ const [agentId = "main", query = "memory"] = rest;
2472
+ await inspectMemory(agentId, query);
2473
+ } else if (command === "shadow-summary") {
2474
+ const [agentId = "main"] = rest;
2475
+ await shadowSummary(agentId);
2476
+ } else if (command === "shadow-import") {
2477
+ const [agentId = "main"] = rest;
2478
+ await shadowImport(agentId);
2479
+ } else if (command === "provider-health") {
2480
+ const [providerId = "ollama"] = rest;
2481
+ await providerHealth(providerId);
2482
+ } else if (command === "provider-capabilities") {
2483
+ const [providerId = "ollama"] = rest;
2484
+ await providerCapabilities(providerId);
2485
+ } else if (command === "manifest-summary") {
2486
+ await manifestSummary();
2487
+ } else if (command === "plan-job") {
2488
+ const [jobId = "heartbeat-check"] = rest;
2489
+ await planJob(jobId);
2490
+ } else if (command === "live-cron-summary") {
2491
+ await liveCronSummary();
2492
+ } else if (command === "compare-jobs") {
2493
+ await compareJobs();
2494
+ } else if (command === "execute-job") {
2495
+ const [jobId = "heartbeat-check", mode = "dry-run", modelOverride = null] = rest;
2496
+ await executeJob(jobId, mode, modelOverride);
2497
+ } else if (command === "execute-job-with-report-fallback") {
2498
+ const [jobId = "workspace-health", mode = "provider", modelOverride = null] = rest;
2499
+ await executeJobWithReportFallback(jobId, mode, modelOverride);
2500
+ } else if (command === "shadow-compare") {
2501
+ const [jobId = "heartbeat-check"] = rest;
2502
+ await shadowCompare(jobId);
2503
+ } else if (command === "provider-mode-policy") {
2504
+ await providerModePolicy();
2505
+ } else if (command === "tick-scheduler") {
2506
+ const [mode = "dry-run"] = rest;
2507
+ await tickScheduler(mode);
2508
+ } else if (command === "due-jobs") {
2509
+ await dueJobs();
2510
+ } else if (command === "review-runs") {
2511
+ const [limit = "10"] = rest;
2512
+ await reviewRuns(limit);
2513
+ } else if (command === "review-notifications") {
2514
+ const [limit = "10"] = rest;
2515
+ await reviewNotifications(limit);
2516
+ } else if (command === "review-deliveries") {
2517
+ const [limit = "10"] = rest;
2518
+ await reviewDeliveries(limit);
2519
+ } else if (command === "review-pending-handoffs") {
2520
+ const [limit = "10"] = rest;
2521
+ await reviewPendingHandoffs(limit);
2522
+ } else if (command === "review-pending-followups") {
2523
+ const [limit = "10"] = rest;
2524
+ await reviewPendingFollowUps(limit);
2525
+ } else if (command === "sweep-pending-followups") {
2526
+ await sweepPendingFollowUps();
2527
+ } else if (command === "sweep-pending-handoffs") {
2528
+ await sweepPendingHandoffs();
2529
+ } else if (command === "deliver-notifications") {
2530
+ const [mode = "shadow", limit = "10"] = rest;
2531
+ await deliverNotifications(mode, limit);
2532
+ } else if (command === "deliver-notification-file") {
2533
+ const [mode = "shadow", notificationFilePath = null] = rest;
2534
+ await deliverNotificationFile(mode, notificationFilePath);
2535
+ } else if (command === "resolve-delivery-profile") {
2536
+ const [profileName = "gateway_preview_main"] = rest;
2537
+ await resolveDeliveryProfile(profileName);
2538
+ } else if (command === "send-test-delivery") {
2539
+ const [profileName = "gateway_preview_main", mode = "shadow", ...messageParts] = rest;
2540
+ await sendTestDelivery(profileName, mode, ...messageParts);
2541
+ } else if (command === "choose-handoff-peer") {
2542
+ const [notificationFilePath = null, peerId = null, deliveryProfile = null] = rest;
2543
+ await chooseHandoffPeer(notificationFilePath, peerId, deliveryProfile);
2544
+ } else if (command === "choose-top-handoff-peer") {
2545
+ const [notificationFilePath = null, deliveryProfile = null] = rest;
2546
+ await chooseTopHandoffPeer(notificationFilePath, deliveryProfile);
2547
+ } else if (command === "consume-follow-up") {
2548
+ const [notificationFilePath = null] = rest;
2549
+ await consumeFollowUp(notificationFilePath);
2550
+ } else if (command === "serve-transport") {
2551
+ const [port = "4318"] = rest;
2552
+ await serveTransport(port);
2553
+ } else if (command === "serve-daemon") {
2554
+ const [mode = "dry-run", intervalMs = "30000"] = rest;
2555
+ await serveDaemon(mode, intervalMs, resolveRuntimeInstallDir());
2556
+ } else if (command === "migrate") {
2557
+ const { runMigration } = await import("./runtime/migration.js");
2558
+ const dryRun = rest.includes("--dry-run");
2559
+ const liveRoot = process.env.NEMORIS_LIVE_ROOT || path.join(process.env.HOME || os.homedir(), ".openclaw");
2560
+ const result = await runMigration({ installDir: projectRoot, liveRoot, dryRun });
2561
+ console.log(JSON.stringify(result, null, 2));
2562
+ return 0;
2563
+ } else if (command === "review-inbox") {
2564
+ const [limit = "10"] = rest;
2565
+ await reviewInbox(limit);
2566
+ } else if (command === "evaluate-runs") {
2567
+ const [limit = "20"] = rest;
2568
+ await evaluateRuns(limit);
2569
+ } else if (command === "evaluate-job") {
2570
+ const [jobId = "heartbeat-check"] = rest;
2571
+ await evaluateJob(jobId);
2572
+ } else if (command === "improvement-targets") {
2573
+ await improvementTargets();
2574
+ } else if (command === "run-improvement") {
2575
+ const [targetId = "workspaceHealth", variantId = "baseline"] = rest;
2576
+ await runImprovement(targetId, variantId);
2577
+ } else if (command === "run-improvement-with-report-fallback") {
2578
+ const [targetId = "workspaceHealth", variantId = "baseline"] = rest;
2579
+ await runImprovementWithReportFallback(targetId, variantId);
2580
+ } else if (command === "compare-improvements") {
2581
+ const [targetId = "workspaceHealth", candidateVariantId = null, baselineVariantId = "baseline"] = rest;
2582
+ await compareImprovements(targetId, candidateVariantId, baselineVariantId);
2583
+ } else if (command === "compare-improvements-with-report-fallback") {
2584
+ const [targetId = "workspaceHealth", candidateVariantId = null, baselineVariantId = "baseline"] = rest;
2585
+ await compareImprovementsWithReportFallback(targetId, candidateVariantId, baselineVariantId);
2586
+ } else if (command === "repair-and-compare-improvement") {
2587
+ const [targetId = "workspaceHealth", candidateVariantId = null, baselineVariantId = "baseline"] = rest;
2588
+ await repairAndCompareImprovement(targetId, candidateVariantId, baselineVariantId);
2589
+ } else if (command === "repair-and-compare-improvement-with-report-fallback") {
2590
+ const [targetId = "workspaceHealth", candidateVariantId = null, baselineVariantId = "baseline"] = rest;
2591
+ await repairAndCompareImprovementWithReportFallback(targetId, candidateVariantId, baselineVariantId);
2592
+ } else if (command === "shadow-eval-job") {
2593
+ const [jobId = "heartbeat-check", modelOverride = null] = rest;
2594
+ await shadowEvalJob(jobId, modelOverride);
2595
+ } else if (command === "benchmark-models") {
2596
+ const [jobId = "workspace-health", ...models] = rest;
2597
+ await benchmarkModels(jobId, models);
2598
+ } else if (command === "index-embeddings") {
2599
+ const [agentId = "heartbeat"] = rest;
2600
+ await indexEmbeddings(agentId);
2601
+ } else if (command === "embedding-readiness") {
2602
+ await embeddingReadiness();
2603
+ } else if (command === "config-validate" || command === "validate-config") {
2604
+ await configValidate();
2605
+ } else if (command === "peer-readiness") {
2606
+ const [peerId = "nemo"] = rest;
2607
+ await peerReadiness(peerId);
2608
+ } else if (command === "dependency-health") {
2609
+ const [jobId = "workspace-health"] = rest;
2610
+ await dependencyHealthForJob(jobId);
2611
+ } else if (command === "report-fallback-readiness") {
2612
+ const [jobId = "workspace-health"] = rest;
2613
+ await reportFallbackReadiness(jobId);
2614
+ } else if (command === "lane-readiness") {
2615
+ const [jobId = "workspace-health"] = rest;
2616
+ await laneReadiness(jobId);
2617
+ } else if (command === "pilot-status") {
2618
+ const [jobId = "workspace-health", limit = "10"] = rest;
2619
+ await pilotStatus(jobId, limit);
2620
+ } else if (command === "cutover-readiness") {
2621
+ const [jobId = "workspace-health", limit = "10"] = rest;
2622
+ await cutoverReadiness(jobId, limit);
2623
+ } else if (command === "daily-driver-trust-pack") {
2624
+ const [limit = "10"] = rest;
2625
+ await dailyDriverTrustPack(limit);
2626
+ } else if (command === "embedding-health") {
2627
+ const [agentId = "heartbeat", probeArg = "false"] = rest;
2628
+ await embeddingHealth(agentId, probeArg === "true" || probeArg === "probe");
2629
+ } else if (command === "job-embedding-health") {
2630
+ const [jobId = "heartbeat-check", probeArg = "false"] = rest;
2631
+ await jobEmbeddingHealth(jobId, probeArg === "true" || probeArg === "probe");
2632
+ } else if (command === "rebuild-job-embeddings") {
2633
+ const [jobId = "heartbeat-check"] = rest;
2634
+ await rebuildJobEmbeddings(jobId);
2635
+ } else if (command === "query-embeddings") {
2636
+ const [agentId = "heartbeat", query = "heartbeat memory"] = rest;
2637
+ await queryEmbeddings(agentId, query);
2638
+ } else if (command === "memory-backends") {
2639
+ const [agentId = "heartbeat"] = rest;
2640
+ await memoryBackends(agentId);
2641
+ } else if (command === "list-agent-cards") {
2642
+ await listAgentCards();
2643
+ } else if (command === "find-agent-cards") {
2644
+ const [query = "research"] = rest;
2645
+ await findAgentCards(query);
2646
+ } else if (command === "suggest-peers") {
2647
+ const [taskType = "workspace_health", query = "workspace health"] = rest;
2648
+ await suggestPeers(taskType, query);
2649
+ } else if (command === "preview-workspace-health-validation") {
2650
+ await previewWorkspaceHealthValidation();
2651
+ } else if (command === "validate-workspace-health") {
2652
+ const [mode = "preview", modelOverride = null] = rest;
2653
+ await validateWorkspaceHealth(mode, modelOverride);
2654
+ } else if (command === "validate-workspace-health-full") {
2655
+ const [selection = "top", modelOverride = null, deliveryProfile = "peer_preview"] = rest;
2656
+ await validateWorkspaceHealthFull(selection, modelOverride, deliveryProfile);
2657
+ } else if (command === "validate-workspace-health-repeatable") {
2658
+ const [selection = "top", modelOverride = null, deliveryProfile = "peer_preview"] = rest;
2659
+ await validateWorkspaceHealthRepeatable(selection, modelOverride, deliveryProfile);
2660
+ } else if (command === "validate-workspace-health-pilot") {
2661
+ const [selection = "top", mode = "dry-run", modelOverride = null, deliveryProfile = "peer_preview"] = rest;
2662
+ await validateWorkspaceHealthPilot(selection, mode, modelOverride, deliveryProfile);
2663
+ } else if (command === "validate-memory-rollup") {
2664
+ const [mode = "provider", modelOverride = null, deliveryMode = "live"] = rest;
2665
+ await validateMemoryRollup(mode, modelOverride, deliveryMode);
2666
+ } else if (command === "query-qmd") {
2667
+ const [agentId = "heartbeat", query = "memory heartbeat"] = rest;
2668
+ await queryQmd(agentId, query);
2669
+ } else if (command === "sqlite-status") {
2670
+ const [agentId = "main"] = rest;
2671
+ await sqliteStatus(agentId);
2672
+ } else if (command === "sqlite-checkpoint") {
2673
+ const [agentId = "main", thresholdBytes = null] = rest;
2674
+ await sqliteCheckpoint(agentId, thresholdBytes);
2675
+ } else if (command === "maintenance-status") {
2676
+ await maintenanceStatus();
2677
+ } else if (command === "prune-state") {
2678
+ const [scope = "all"] = rest;
2679
+ await pruneState(scope);
2680
+ } else if (command === "runtime-status") {
2681
+ const jsonFlag = rest.includes("--json");
2682
+ await runtimeStatus(jsonFlag);
2683
+ } else if (command === "models") {
2684
+ await showModelsOverview(resolveRuntimeInstallDir());
2685
+ } else if (command === "setup" && rest[0] === "telegram") {
2686
+ await setupTelegram();
2687
+ } else if (command === "telegram" && rest[0] === "whoami") {
2688
+ await telegramWhoami();
2689
+ } else if (command === "uninstall") {
2690
+ const { runUninstall } = await import("./onboarding/uninstall.js");
2691
+ const force = rest.includes("--force") || rest.includes("-f");
2692
+ return runUninstall({ installDir: resolveRuntimeInstallDir(), force });
2693
+ } else if (command === "setup") {
2694
+ const { runWizard } = await import("./onboarding/wizard.js");
2695
+ const nonInteractive = rest.includes("--non-interactive");
2696
+ const advanced = rest.includes("--advanced");
2697
+ return runWizard({
2698
+ installDir: resolveRuntimeInstallDir(),
2699
+ nonInteractive,
2700
+ advanced,
2701
+ acceptRisk: rest.includes("--accept-risk"),
2702
+ flow: parseFlagValue(rest, "--flow"),
2703
+ anthropicKey: parseFlagValue(rest, "--anthropic-key"),
2704
+ openaiKey: parseFlagValue(rest, "--openai-key"),
2705
+ openrouterKey: parseFlagValue(rest, "--openrouter-key"),
2706
+ telegramToken: parseFlagValue(rest, "--telegram-token"),
2707
+ });
2708
+ } else if (command === "init") {
2709
+ const { runWizard } = await import("./onboarding/wizard.js");
2710
+ const nonInteractive = rest.includes("--non-interactive");
2711
+ const advanced = rest.includes("--advanced");
2712
+ return runWizard({
2713
+ installDir: resolveRuntimeInstallDir(),
2714
+ nonInteractive,
2715
+ advanced,
2716
+ acceptRisk: rest.includes("--accept-risk"),
2717
+ flow: parseFlagValue(rest, "--flow"),
2718
+ anthropicKey: parseFlagValue(rest, "--anthropic-key"),
2719
+ openaiKey: parseFlagValue(rest, "--openai-key"),
2720
+ openrouterKey: parseFlagValue(rest, "--openrouter-key"),
2721
+ telegramToken: parseFlagValue(rest, "--telegram-token"),
2722
+ });
2723
+ } else if (command === "doctor") {
2724
+ const { runDoctor, formatDoctorReport } = await import("./onboarding/doctor.js");
2725
+ const results = await runDoctor(resolveRuntimeInstallDir());
2726
+ const json = rest.includes("--json");
2727
+ if (json) {
2728
+ console.log(JSON.stringify(results, null, 2));
2729
+ } else {
2730
+ console.log(formatDoctorReport(results));
2731
+ }
2732
+ return results.exitCode;
2733
+ } else if (command === "battle") {
2734
+ const { runBattle, parseBattleFlags } = await import("./battle.js");
2735
+ const flags = parseBattleFlags(rest);
2736
+ await runBattle(flags);
2737
+ } else if (command === "mcp") {
2738
+ if (rest.length === 0 || rest[0] === "list") {
2739
+ await mcpList();
2740
+ } else {
2741
+ writeOutput(process.stderr, formatCommandHelp("mcp"));
2742
+ process.exitCode = 1;
2743
+ return 1;
2744
+ }
2745
+ } else if (command === "tools") {
2746
+ const [sub, toolId] = rest;
2747
+ if (sub === "add") {
2748
+ await addTool(toolId);
2749
+ } else {
2750
+ await listTools();
2751
+ }
2752
+ } else if (command === "skills") {
2753
+ await listSkills();
2754
+ } else if (command === "improvements") {
2755
+ await listImprovements();
2756
+ } else {
2757
+ writeOutput(process.stderr, formatGeneralHelp({ includeInternal: shouldShowAllCommands(rest) }));
2758
+ process.exitCode = 1;
2759
+ return 1;
2760
+ }
2761
+
2762
+ return process.exitCode || 0;
2763
+ }