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,347 @@
1
+ import { parseContractOutput } from "./output-contract-validator.js";
2
+
3
+ function dedupe(items) {
4
+ return [...new Set((items || []).map((item) => String(item).trim()).filter(Boolean))];
5
+ }
6
+
7
+ function normalizeSectionKey(value) {
8
+ return String(value || "")
9
+ .toLowerCase()
10
+ .replace(/[\s-]+/g, "_")
11
+ .trim();
12
+ }
13
+
14
+ function normalizeText(value) {
15
+ return String(value || "").replace(/\s+/g, " ").trim();
16
+ }
17
+
18
+ function trimValue(value, limit = 220) {
19
+ const text = normalizeText(value);
20
+ if (text.length <= limit) return text;
21
+ return `${text.slice(0, limit - 3)}...`;
22
+ }
23
+
24
+ function normalizeNextActions(nextActions = []) {
25
+ return (nextActions || [])
26
+ .map((item) => {
27
+ if (typeof item === "string") return normalizeText(item);
28
+ if (item && typeof item === "object") {
29
+ const action = normalizeText(item.action || "");
30
+ const details = normalizeText(item.details || item.summary || "");
31
+ return [action, details].filter(Boolean).join(": ");
32
+ }
33
+ return normalizeText(item);
34
+ })
35
+ .filter(Boolean);
36
+ }
37
+
38
+ export function normalizeInteractionContract(contract = null) {
39
+ const raw = contract || {};
40
+ const progressAfterSeconds = raw.progressAfterSeconds ?? 90;
41
+ const handoff = raw.handoff || {};
42
+ const yieldConfig = raw.yield || {};
43
+
44
+ return {
45
+ ackMode: raw.ackMode || "immediate",
46
+ ackText: raw.ackText || null,
47
+ progressMode: raw.progressMode || "long_running",
48
+ progressAfterSeconds,
49
+ maxSilenceSeconds: raw.maxSilenceSeconds ?? Math.max(progressAfterSeconds * 2, 180),
50
+ notifyOnDone: raw.notifyOnDone ?? true,
51
+ notifyOnError: raw.notifyOnError ?? true,
52
+ requiresPingback: raw.requiresPingback ?? true,
53
+ pingbackTarget: raw.pingbackTarget || "same_thread",
54
+ completionSignal: raw.completionSignal || "done",
55
+ failureSignal: raw.failureSignal || "error",
56
+ handoffFormat: raw.handoffFormat || "structured_handoff",
57
+ completionSections: dedupe(raw.completionSections || ["status", "next_actions"]),
58
+ includeNextActions: raw.includeNextActions ?? true,
59
+ handoff: {
60
+ enabled: handoff.enabled ?? false,
61
+ signal: handoff.signal || raw.handoffSignal || "peer_handoff",
62
+ target: handoff.target || raw.handoffTarget || null,
63
+ deliveryProfile: handoff.deliveryProfile || raw.handoffDeliveryProfile || null,
64
+ capabilityQuery: handoff.capabilityQuery || raw.handoffCapabilityQuery || null,
65
+ preferredTaskClasses: dedupe(handoff.preferredTaskClasses || raw.handoffPreferredTaskClasses || []),
66
+ trustLevel: handoff.trustLevel || raw.handoffTrustLevel || null,
67
+ maxSuggestions: handoff.maxSuggestions ?? raw.handoffMaxSuggestions ?? 3,
68
+ messagePrefix: handoff.messagePrefix || raw.handoffMessagePrefix || null
69
+ },
70
+ yield: {
71
+ enabled: yieldConfig.enabled ?? raw.yieldEnabled ?? false,
72
+ signal: yieldConfig.signal || raw.yieldSignal || "follow_up",
73
+ targetSurface: yieldConfig.targetSurface || raw.yieldTargetSurface || "operator_review",
74
+ followUpObjective: yieldConfig.followUpObjective || raw.followUpObjective || null
75
+ }
76
+ };
77
+ }
78
+
79
+ export function resolveInteractionContract(agentContract = null, jobContract = null) {
80
+ return normalizeInteractionContract({
81
+ ...(agentContract || {}),
82
+ ...(jobContract || {})
83
+ });
84
+ }
85
+
86
+ export function formatInteractionContract(contract) {
87
+ if (!contract) return "";
88
+
89
+ const lines = [
90
+ `- ack mode: ${contract.ackMode}`,
91
+ `- progress mode: ${contract.progressMode}`,
92
+ `- progress after seconds: ${contract.progressAfterSeconds}`,
93
+ `- max silence seconds: ${contract.maxSilenceSeconds}`,
94
+ `- requires pingback: ${contract.requiresPingback}`,
95
+ `- pingback target: ${contract.pingbackTarget}`,
96
+ `- notify on done: ${contract.notifyOnDone}`,
97
+ `- notify on error: ${contract.notifyOnError}`,
98
+ `- completion signal: ${contract.completionSignal}`,
99
+ `- handoff format: ${contract.handoffFormat}`
100
+ ];
101
+
102
+ if (contract.completionSections?.length) {
103
+ lines.push(`- completion sections: ${contract.completionSections.join(", ")}`);
104
+ }
105
+ if (contract.handoff?.enabled) {
106
+ lines.push(`- handoff enabled: yes`);
107
+ if (contract.handoff.capabilityQuery) lines.push(`- handoff capability query: ${contract.handoff.capabilityQuery}`);
108
+ if (contract.handoff.preferredTaskClasses?.length) {
109
+ lines.push(`- handoff preferred tasks: ${contract.handoff.preferredTaskClasses.join(", ")}`);
110
+ }
111
+ if (contract.handoff.target) {
112
+ lines.push(`- handoff target: ${typeof contract.handoff.target === "string" ? contract.handoff.target : JSON.stringify(contract.handoff.target)}`);
113
+ }
114
+ }
115
+ if (contract.yield?.enabled) {
116
+ lines.push(`- yield enabled: yes`);
117
+ lines.push(`- yield target surface: ${contract.yield.targetSurface}`);
118
+ }
119
+
120
+ return lines.join("\n");
121
+ }
122
+
123
+ function outputSectionMap(output, result, contract = null) {
124
+ const entries = [];
125
+ if (contract?.requiredSections?.length) {
126
+ const parsed = parseContractOutput(output, contract);
127
+ for (const section of contract.requiredSections) {
128
+ const key = normalizeSectionKey(section);
129
+ entries.push([key, trimValue(parsed.sections.get(key) || "None")]);
130
+ }
131
+ } else if (output && typeof output === "object" && !Array.isArray(output)) {
132
+ for (const [key, value] of Object.entries(output)) {
133
+ entries.push([normalizeSectionKey(key), trimValue(value)]);
134
+ }
135
+ }
136
+
137
+ entries.push(["status", trimValue(result.summary || "Completed.")]);
138
+ entries.push(["summary", trimValue(result.summary || "Completed.")]);
139
+ const nextActions = normalizeNextActions(result.nextActions || []);
140
+ entries.push(["next_actions", nextActions.length ? nextActions.join("; ") : "None"]);
141
+ entries.push(["next", nextActions.length ? nextActions.join("; ") : "None"]);
142
+ entries.push(["handoff", trimValue(typeof output === "string" ? output : JSON.stringify(output || result.output || ""))]);
143
+ entries.push(["evidence", trimValue(typeof output === "string" ? output : JSON.stringify(output || result.output || ""))]);
144
+ entries.push(["changes", trimValue(typeof output === "string" ? output : JSON.stringify(output || result.output || ""))]);
145
+ entries.push(["verification", nextActions.length ? nextActions.join("; ") : "No explicit verification captured."]);
146
+ entries.push(["issues", "None"]);
147
+
148
+ return new Map(entries);
149
+ }
150
+
151
+ function buildCompletionSections(contract, result) {
152
+ const output = result.output;
153
+ const map = outputSectionMap(output, result, result.outputContract || null);
154
+
155
+ return contract.completionSections.map((section) => ({
156
+ key: normalizeSectionKey(section),
157
+ label: String(section)
158
+ .replace(/_/g, " ")
159
+ .replace(/\b\w/g, (char) => char.toUpperCase()),
160
+ value: map.get(normalizeSectionKey(section)) || "None"
161
+ }));
162
+ }
163
+
164
+ function renderCompletionMessage(plan, contract, sections) {
165
+ if (contract.handoffFormat === "concise_status") {
166
+ const status = sections.find((section) => section.key === "status")?.value || plan.job.id;
167
+ const next = sections.find((section) => section.key === "next_actions")?.value || "None";
168
+ return `[${contract.completionSignal}] ${status} | next: ${next}`;
169
+ }
170
+
171
+ if (contract.handoffFormat === "coding_completion") {
172
+ const lines = [`${contract.completionSignal.toUpperCase()}: ${plan.job.id}`];
173
+ for (const section of sections) {
174
+ lines.push(`${section.label}: ${section.value}`);
175
+ }
176
+ return lines.join("\n");
177
+ }
178
+
179
+ return [
180
+ `${contract.completionSignal.toUpperCase()}: ${plan.job.id}`,
181
+ ...sections.map((section) => `- ${section.label}: ${section.value}`)
182
+ ].join("\n");
183
+ }
184
+
185
+ function normalizeHandoffTarget(target, deliveryProfile = null) {
186
+ if (!target) return null;
187
+ if (typeof target === "string") {
188
+ return {
189
+ mode: "peer_agent",
190
+ peerId: target,
191
+ deliveryProfile
192
+ };
193
+ }
194
+ return {
195
+ ...target,
196
+ deliveryProfile: target.deliveryProfile || deliveryProfile || null
197
+ };
198
+ }
199
+
200
+ function buildHandoffMessage(plan, contract, sections, suggestions = []) {
201
+ const status = sections.find((section) => section.key === "status")?.value || resultSummaryFallback(plan);
202
+ const nextActions = sections.find((section) => section.key === "next_actions")?.value || "None";
203
+ const lines = [];
204
+
205
+ if (contract.handoff.messagePrefix) {
206
+ lines.push(contract.handoff.messagePrefix);
207
+ }
208
+ lines.push(`[${contract.handoff.signal}] ${plan.job.id}`);
209
+ lines.push(`Status: ${status}`);
210
+ lines.push(`Next Actions: ${nextActions}`);
211
+
212
+ if (suggestions.length) {
213
+ lines.push(`Suggested Peers: ${suggestions.map((item) => item.label).join(", ")}`);
214
+ }
215
+
216
+ return lines.join("\n");
217
+ }
218
+
219
+ function resultSummaryFallback(plan) {
220
+ return `${plan.job.id} completed.`;
221
+ }
222
+
223
+ function buildHandoffPlan(plan, contract, sections, suggestions = []) {
224
+ if (!contract.handoff?.enabled) return null;
225
+
226
+ const target = normalizeHandoffTarget(contract.handoff.target, contract.handoff.deliveryProfile);
227
+ return {
228
+ required: true,
229
+ signal: contract.handoff.signal,
230
+ target,
231
+ deliveryProfile: contract.handoff.deliveryProfile || target?.deliveryProfile || null,
232
+ suggestions: suggestions.slice(0, contract.handoff.maxSuggestions || 3),
233
+ capabilityQuery: contract.handoff.capabilityQuery || null,
234
+ preferredTaskClasses: contract.handoff.preferredTaskClasses || [],
235
+ sections,
236
+ nextActions: contract.includeNextActions ? sections.find((section) => section.key === "next_actions")?.value?.split("; ").filter(Boolean) || [] : [],
237
+ message: buildHandoffMessage(plan, contract, sections, suggestions)
238
+ };
239
+ }
240
+
241
+ function buildYieldPlan(plan, contract, result, completion, handoff) {
242
+ if (!contract.yield?.enabled) return null;
243
+
244
+ return {
245
+ required: true,
246
+ signal: contract.yield.signal,
247
+ targetSurface: contract.yield.targetSurface || "operator_review",
248
+ objective:
249
+ contract.yield.followUpObjective ||
250
+ `Continue ${plan.job.id} follow-up in a bounded next step without extending the current turn.`,
251
+ followUp: {
252
+ sourceJobId: plan.job.id,
253
+ sourceAgentId: plan.agent?.id || plan.packet?.agentId || null,
254
+ signal: contract.yield.signal,
255
+ objective:
256
+ contract.yield.followUpObjective ||
257
+ `Continue ${plan.job.id} follow-up in a bounded next step without extending the current turn.`,
258
+ targetSurface: contract.yield.targetSurface || "operator_review",
259
+ sections: completion?.sections || [],
260
+ nextActions: result.nextActions || [],
261
+ completion: completion?.required
262
+ ? {
263
+ signal: completion.signal,
264
+ target: completion.target,
265
+ handoffFormat: completion.handoffFormat,
266
+ sections: completion.sections || [],
267
+ nextActions: completion.nextActions || [],
268
+ message: completion.message
269
+ }
270
+ : null,
271
+ handoff: handoff?.required
272
+ ? {
273
+ signal: handoff.signal,
274
+ target: handoff.target || null,
275
+ deliveryProfile: handoff.deliveryProfile || null,
276
+ suggestions: handoff.suggestions || [],
277
+ capabilityQuery: handoff.capabilityQuery || null,
278
+ preferredTaskClasses: handoff.preferredTaskClasses || [],
279
+ sections: handoff.sections || [],
280
+ nextActions: handoff.nextActions || [],
281
+ message: handoff.message
282
+ }
283
+ : null
284
+ }
285
+ };
286
+ }
287
+
288
+ export function buildInteractionPlan(plan, result, error = null, options = {}) {
289
+ const contract = normalizeInteractionContract(plan.packet.layers.interactionContract);
290
+ result.outputContract = plan.packet.layers.outputContract || null;
291
+
292
+ const ack =
293
+ contract.ackMode === "silent"
294
+ ? {
295
+ required: false,
296
+ mode: contract.ackMode,
297
+ target: contract.pingbackTarget,
298
+ message: null
299
+ }
300
+ : {
301
+ required: true,
302
+ mode: contract.ackMode,
303
+ target: contract.pingbackTarget,
304
+ message:
305
+ contract.ackText ||
306
+ `Acknowledged ${plan.job.id}. I will ping back with ${contract.completionSignal} when the run is finished.`
307
+ };
308
+
309
+ const completionSections = buildCompletionSections(contract, result);
310
+ const handoff = buildHandoffPlan(plan, contract, completionSections, options.handoffSuggestions || []);
311
+ const completion = {
312
+ required: contract.notifyOnDone || contract.requiresPingback,
313
+ signal: contract.completionSignal,
314
+ target: contract.pingbackTarget,
315
+ handoffFormat: contract.handoffFormat,
316
+ sections: completionSections,
317
+ nextActions: contract.includeNextActions ? result.nextActions || [] : [],
318
+ message: renderCompletionMessage(plan, contract, completionSections)
319
+ };
320
+ const yieldPlan = error ? null : buildYieldPlan(plan, contract, result, completion, handoff);
321
+
322
+ return {
323
+ ack,
324
+ progress: {
325
+ enabled: contract.progressMode !== "none",
326
+ mode: contract.progressMode,
327
+ afterSeconds: contract.progressAfterSeconds,
328
+ maxSilenceSeconds: contract.maxSilenceSeconds,
329
+ target: contract.pingbackTarget,
330
+ message:
331
+ contract.progressMode === "none"
332
+ ? null
333
+ : `Still working on ${plan.job.id}. Send a progress ping before ${contract.maxSilenceSeconds}s of silence.`
334
+ },
335
+ completion,
336
+ handoff,
337
+ yield: yieldPlan,
338
+ error: {
339
+ required: contract.notifyOnError,
340
+ signal: contract.failureSignal,
341
+ target: contract.pingbackTarget,
342
+ message: error
343
+ ? `[${contract.failureSignal}] ${plan.job.id}: ${trimValue(error.message || error)}`
344
+ : null
345
+ }
346
+ };
347
+ }
@@ -0,0 +1,226 @@
1
+ function truthy(value) {
2
+ return value === true;
3
+ }
4
+
5
+ function hasBlockingInteractionIssues(interaction = null) {
6
+ if (!interaction) return false;
7
+ return Boolean(
8
+ (interaction.deliveryEvidenceRequired && !interaction.deliveryEvidenceHealthy) ||
9
+ (interaction.yielded && interaction.followUpQueued && !interaction.followUpConsumed) ||
10
+ interaction.followUpExpired ||
11
+ interaction.handoffExpired ||
12
+ interaction.handoffBlocked ||
13
+ (interaction.handoffRequired && interaction.handoffQueued && !interaction.handoffChosen && !interaction.handoffPendingChoice) ||
14
+ interaction.deliveryUncertain
15
+ );
16
+ }
17
+
18
+ function summarizeFallback(reportFallback = null) {
19
+ if (!reportFallback) {
20
+ return {
21
+ configured: false,
22
+ ready: true,
23
+ blockedReasons: []
24
+ };
25
+ }
26
+
27
+ return {
28
+ configured: Boolean(reportFallback.jobConfigured),
29
+ ready: Boolean(!reportFallback.jobConfigured || reportFallback.ready),
30
+ blockedReasons: reportFallback.blockedReasons || []
31
+ };
32
+ }
33
+
34
+ function isStaleError(health) {
35
+ if (!health.lastError) return false;
36
+ if (!health.lastSuccessAt) return false;
37
+ const successMs = new Date(health.lastSuccessAt).getTime();
38
+ const errorMs = health.lastErrorAt ? new Date(health.lastErrorAt).getTime() : 0;
39
+ return successMs > errorMs;
40
+ }
41
+
42
+ function summarizeEmbeddings(embeddingHealth = null) {
43
+ const health = embeddingHealth?.embeddingHealth || embeddingHealth || null;
44
+ if (!health) {
45
+ return {
46
+ available: false,
47
+ healthy: false,
48
+ degraded: true,
49
+ blockedReasons: ["embedding_health_missing"]
50
+ };
51
+ }
52
+
53
+ const blockedReasons = [];
54
+ if (health.disabled) blockedReasons.push("embeddings_disabled");
55
+ if (health.lastError && !isStaleError(health)) blockedReasons.push("embedding_error");
56
+ if (health.missingCount > 0 && health.freshCount === 0) blockedReasons.push("embeddings_missing");
57
+ if (health.failedCount > 0) blockedReasons.push("embedding_failures");
58
+
59
+ return {
60
+ available: !health.disabled,
61
+ healthy: blockedReasons.length === 0,
62
+ degraded: blockedReasons.length > 0,
63
+ blockedReasons,
64
+ health
65
+ };
66
+ }
67
+
68
+ function reconcileEmbeddingsWithRetrieval(embeddings, retrieval) {
69
+ if (!embeddings) return embeddings;
70
+ if (!retrieval) return embeddings;
71
+
72
+ const retrievalShowsHealthyEmbeddingQuery =
73
+ retrieval.embeddingQueryMode === "embedding_query" &&
74
+ Number(retrieval.semanticCount || 0) > 0 &&
75
+ !retrieval.embeddingError &&
76
+ Number(retrieval.failedEmbeddingCount || 0) === 0;
77
+
78
+ if (!retrievalShowsHealthyEmbeddingQuery) {
79
+ return embeddings;
80
+ }
81
+
82
+ const blockedReasons = (embeddings.blockedReasons || []).filter((reason) => reason !== "embedding_error");
83
+ return {
84
+ ...embeddings,
85
+ healthy: blockedReasons.length === 0,
86
+ degraded: blockedReasons.length > 0,
87
+ blockedReasons
88
+ };
89
+ }
90
+
91
+ function buildEffectiveRetrieval(retrieval = null, embeddings = null) {
92
+ if (!retrieval) return null;
93
+
94
+ const effective = { ...retrieval };
95
+ const embeddingHealth = embeddings?.health || null;
96
+
97
+ // Only adopt the embedding health's lastQueryMode when the evaluation's
98
+ // own retrieval doesn't already show a healthy semantic result. This
99
+ // prevents a stale "lexical_fallback" from overwriting a genuinely
100
+ // healthy "embedding_query" captured during the most recent run.
101
+ const evalAlreadyHealthy =
102
+ retrieval.embeddingQueryMode === "embedding_query" &&
103
+ Number(retrieval.semanticCount || 0) > 0 &&
104
+ !retrieval.embeddingError;
105
+
106
+ if (embeddingHealth?.lastQueryMode && !evalAlreadyHealthy) {
107
+ effective.embeddingQueryMode = embeddingHealth.lastQueryMode;
108
+ }
109
+
110
+ if (embeddings?.healthy && !embeddings.degraded) {
111
+ effective.embeddingError = null;
112
+ effective.failedEmbeddingCount = 0;
113
+ }
114
+
115
+ return effective;
116
+ }
117
+
118
+ export function buildLaneReadiness({
119
+ jobId,
120
+ job = null,
121
+ evaluation = null,
122
+ embeddingHealth = null,
123
+ reportFallback = null,
124
+ maintenance = null,
125
+ dependencyHealth = null
126
+ } = {}) {
127
+ const interaction = evaluation?.interaction || null;
128
+ const retrieval = evaluation?.retrieval || null;
129
+ const contractCheck = evaluation?.contractCheck || null;
130
+ const rubric = evaluation?.rubric || null;
131
+ const embeddingSummary = summarizeEmbeddings(embeddingHealth);
132
+ const effectiveRetrieval = buildEffectiveRetrieval(retrieval, embeddingSummary);
133
+ const embeddings = reconcileEmbeddingsWithRetrieval(embeddingSummary, effectiveRetrieval);
134
+ const fallback = summarizeFallback(reportFallback);
135
+
136
+ const checks = {
137
+ contractHealthy:
138
+ contractCheck == null
139
+ ? true
140
+ : Number(contractCheck.satisfiedRatio ?? 0) >= 0.99 && (contractCheck.missingFromV2 || []).length === 0,
141
+ interactionHealthy: !hasBlockingInteractionIssues(interaction),
142
+ retrievalHealthy:
143
+ !effectiveRetrieval ||
144
+ (!effectiveRetrieval.embeddingError &&
145
+ effectiveRetrieval.embeddingQueryMode !== "lexical_fallback" &&
146
+ Number(effectiveRetrieval.failedEmbeddingCount || 0) === 0),
147
+ daemonHealthy:
148
+ !maintenance ||
149
+ ((maintenance.handoffs?.expiredCount || 0) === 0 && (maintenance.followUps?.expiredCount || 0) === 0),
150
+ providerReachable:
151
+ !dependencyHealth || dependencyHealth.provider?.reachable !== false,
152
+ deliveryReachable:
153
+ !dependencyHealth || dependencyHealth.delivery?.reachable !== false,
154
+ fallbackReady: fallback.ready
155
+ };
156
+
157
+ const blockers = [];
158
+ if (!checks.contractHealthy) blockers.push("output_contract_not_consistently_satisfied");
159
+ if (!checks.interactionHealthy) blockers.push("interaction_lifecycle_has_blocking_issues");
160
+ if (!checks.retrievalHealthy) blockers.push("retrieval_is_degraded_or_falling_back");
161
+ if (!checks.daemonHealthy) blockers.push("daemon_maintenance_detected_expired_items");
162
+ if (!checks.providerReachable) blockers.push("provider_unreachable");
163
+ if (embeddings.degraded) blockers.push(...embeddings.blockedReasons.map((item) => `embeddings:${item}`));
164
+
165
+ const warnings = [];
166
+ if (!checks.deliveryReachable) warnings.push("delivery_not_reachable");
167
+
168
+ const suggestions = [];
169
+ if (!checks.retrievalHealthy) {
170
+ suggestions.push("Repair embeddings or accept lexical-only mode before treating this lane as daily-driver ready.");
171
+ }
172
+ if (!checks.contractHealthy) {
173
+ suggestions.push("Keep contract adherence at 100% before cutover.");
174
+ }
175
+ if (!checks.interactionHealthy) {
176
+ suggestions.push("Resolve follow-up / handoff lifecycle issues before using this lane unattended.");
177
+ }
178
+ if (!checks.providerReachable) {
179
+ suggestions.push("Check provider health before treating this lane as trustworthy.");
180
+ }
181
+ if (!checks.deliveryReachable) {
182
+ suggestions.push("Delivery transport is not reachable. This does not block pilot readiness but should be resolved before daily-driver cutover.");
183
+ }
184
+ if (!checks.fallbackReady && fallback.configured) {
185
+ suggestions.push("Remote fallback is optional for this lane. Enable and validate it only if you want recovery from local failures.");
186
+ }
187
+ if (checks.contractHealthy && checks.interactionHealthy && checks.retrievalHealthy) {
188
+ suggestions.push("This lane is technically close to cutover; focus next on repeat-run validation and operator trust.");
189
+ }
190
+
191
+ let tier = "not_ready";
192
+ if (blockers.length === 0 && Number(rubric?.overallScore || 0) >= 0.95) {
193
+ tier = "ready_for_pilot";
194
+ } else if (blockers.length <= 2 && Number(rubric?.overallScore || 0) >= 0.85) {
195
+ tier = "close";
196
+ }
197
+
198
+ return {
199
+ jobId,
200
+ taskType: job?.taskType || null,
201
+ primaryLane: job?.modelLane || null,
202
+ readinessTier: tier,
203
+ readyForPilot: tier === "ready_for_pilot",
204
+ checks,
205
+ blockers: [...new Set(blockers)],
206
+ warnings,
207
+ suggestions,
208
+ summary: {
209
+ overallScore: rubric?.overallScore ?? null,
210
+ contractSatisfiedRatio: contractCheck?.satisfiedRatio ?? null,
211
+ yielded: truthy(interaction?.yielded),
212
+ followUpConsumed: truthy(interaction?.followUpConsumed),
213
+ handoffDelivered: truthy(interaction?.handoffDelivered),
214
+ embeddingQueryMode: effectiveRetrieval?.embeddingQueryMode || null,
215
+ semanticCount: effectiveRetrieval?.semanticCount ?? 0,
216
+ fallbackConfigured: fallback.configured,
217
+ fallbackReady: fallback.ready
218
+ },
219
+ interaction,
220
+ retrieval: effectiveRetrieval,
221
+ embeddings,
222
+ fallback,
223
+ maintenance,
224
+ dependencyHealth
225
+ };
226
+ }