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,439 @@
1
+ function normalizeText(value) {
2
+ return String(value || "")
3
+ .replace(/\s+/g, " ")
4
+ .trim();
5
+ }
6
+
7
+ function normalizeInline(value) {
8
+ return normalizeText(value) || "None";
9
+ }
10
+
11
+ function normalizeSectionContent(value) {
12
+ if (Array.isArray(value)) {
13
+ const items = value.map((item) => normalizeText(item)).filter(Boolean);
14
+ return items.length ? items.join("; ") : "None";
15
+ }
16
+ if (value && typeof value === "object") {
17
+ const text = Object.values(value).map((item) => normalizeText(item)).filter(Boolean).join("; ");
18
+ return text || "None";
19
+ }
20
+ return normalizeInline(value);
21
+ }
22
+
23
+ function stripOuterFence(text) {
24
+ const raw = String(text || "").trim();
25
+ const fencedMatch = raw.match(/^```(?:json|markdown|md|text)?\s*([\s\S]*?)\s*```$/i);
26
+ return fencedMatch ? fencedMatch[1].trim() : raw;
27
+ }
28
+
29
+ function parseJsonCandidate(text) {
30
+ const candidate = stripOuterFence(text);
31
+ if (!(candidate.startsWith("{") && candidate.endsWith("}"))) {
32
+ return null;
33
+ }
34
+
35
+ try {
36
+ return JSON.parse(candidate);
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ function normalizeSectionKey(value) {
43
+ return String(value || "")
44
+ .toLowerCase()
45
+ .replace(/^#+\s*/, "")
46
+ .replace(/[*:_-]+/g, " ")
47
+ .replace(/\s+/g, " ")
48
+ .trim();
49
+ }
50
+
51
+ const SECTION_ALIASES = new Map([
52
+ ["project", "projects"],
53
+ ["next action", "next actions"]
54
+ ]);
55
+
56
+ function canonicalSectionKey(value) {
57
+ const normalized = normalizeSectionKey(value);
58
+ return SECTION_ALIASES.get(normalized) || normalized;
59
+ }
60
+
61
+ function getContractRules(contract = null) {
62
+ const profile = contract?.profile || null;
63
+ return {
64
+ requireStatus: profile?.requireStatus ?? (contract?.format === "bulleted_briefing"),
65
+ sectionStyle: profile?.sectionStyle || (contract?.format === "structured_rollup" ? "headings" : "bullets"),
66
+ requireSectionItems: profile?.requireSectionItems ?? true
67
+ };
68
+ }
69
+
70
+ function parseObjectOutput(value, contract) {
71
+ const entries = Object.entries(value || {});
72
+ const sections = new Map();
73
+ let status = null;
74
+ const rules = getContractRules(contract);
75
+
76
+ for (const [key, entryValue] of entries) {
77
+ const normalizedKey = canonicalSectionKey(key);
78
+ const normalizedValue = normalizeSectionContent(entryValue);
79
+ if (normalizedKey === "status") {
80
+ status = normalizedValue;
81
+ continue;
82
+ }
83
+ sections.set(normalizedKey, normalizedValue);
84
+ }
85
+
86
+ if (rules.requireStatus && !status) {
87
+ status = "None";
88
+ }
89
+
90
+ return {
91
+ source: "object",
92
+ status,
93
+ sections,
94
+ bulletSections: rules.sectionStyle === "bullets" ? sections.size : 0,
95
+ headingSections: rules.sectionStyle === "headings" ? sections.size : 0
96
+ };
97
+ }
98
+
99
+ function buildCanonicalSections(contract, value, options = {}) {
100
+ const parsed = parseContractOutput(value, contract);
101
+ const requiredSections = contract?.requiredSections || [];
102
+ const sections = requiredSections.map((section) => {
103
+ const key = canonicalSectionKey(section);
104
+ return {
105
+ key,
106
+ label: section,
107
+ value: normalizeSectionContent(parsed.sections.get(key) || "None")
108
+ };
109
+ });
110
+
111
+ if (
112
+ contract?.format === "structured_rollup" &&
113
+ options.shadowOnlyEvidence &&
114
+ sections.every((section) => section.key === "update" || section.value === "None")
115
+ ) {
116
+ const updateSection = sections.find((section) => section.key === "update");
117
+ if (updateSection && updateSection.value === "None") {
118
+ updateSection.value = "Shadow import completed; monitor for new docs or memory before the next rollup.";
119
+ }
120
+ }
121
+
122
+ return {
123
+ parsed,
124
+ sections
125
+ };
126
+ }
127
+
128
+ export function renderContractOutput(contract, value, options = {}) {
129
+ if (!contract) return typeof value === "string" ? stripOuterFence(value) : normalizeSectionContent(value);
130
+
131
+ const { parsed, sections } = buildCanonicalSections(contract, value, options);
132
+ const rules = getContractRules(contract);
133
+
134
+ if (rules.sectionStyle === "headings") {
135
+ return sections
136
+ .map((section) => `## ${section.label.replace(/\b\w/g, (char) => char.toUpperCase())}\n- ${section.value}`)
137
+ .join("\n\n")
138
+ .trim();
139
+ }
140
+
141
+ const lines = [];
142
+ if (rules.requireStatus) {
143
+ lines.push(`Status: ${normalizeSectionContent(parsed.status || options.status || "None")}`);
144
+ }
145
+ for (const section of sections) {
146
+ lines.push(`- ${section.label.replace(/\b\w/g, (char) => char.toUpperCase())}: ${section.value}`);
147
+ }
148
+ return lines.join("\n").trim();
149
+ }
150
+
151
+ function parseBulletedBriefing(text) {
152
+ const lines = String(text || "")
153
+ .split("\n")
154
+ .map((line) => line.trim())
155
+ .filter(Boolean);
156
+ const sections = new Map();
157
+ let status = null;
158
+ let bulletSections = 0;
159
+
160
+ for (const line of lines) {
161
+ const statusMatch = line.match(/^status:\s*(.+)$/i);
162
+ if (statusMatch) {
163
+ status = normalizeInline(statusMatch[1]);
164
+ continue;
165
+ }
166
+
167
+ const bulletMatch = line.match(/^[-*]\s+\**([^:*]+?)\**:\s*(.+)$/);
168
+ if (bulletMatch) {
169
+ sections.set(canonicalSectionKey(bulletMatch[1]), normalizeInline(bulletMatch[2]));
170
+ bulletSections += 1;
171
+ continue;
172
+ }
173
+
174
+ const plainMatch = line.match(/^([^:]{2,40}):\s*(.+)$/);
175
+ if (plainMatch) {
176
+ const key = canonicalSectionKey(plainMatch[1]);
177
+ const value = normalizeInline(plainMatch[2]);
178
+ if (key === "status") status = value;
179
+ else sections.set(key, value);
180
+ }
181
+ }
182
+
183
+ return {
184
+ source: "text",
185
+ status,
186
+ sections,
187
+ bulletSections,
188
+ headingSections: 0
189
+ };
190
+ }
191
+
192
+ function parseStructuredRollup(text) {
193
+ const lines = String(text || "")
194
+ .split("\n")
195
+ .map((line) => line.trim());
196
+ const sections = new Map();
197
+ let current = null;
198
+ let headingSections = 0;
199
+ let bulletSections = 0;
200
+
201
+ for (const line of lines) {
202
+ if (!line) continue;
203
+
204
+ const headingMatch = line.match(/^#{1,6}\s+(.+)$/);
205
+ if (headingMatch) {
206
+ current = canonicalSectionKey(headingMatch[1]);
207
+ if (!sections.has(current)) sections.set(current, []);
208
+ headingSections += 1;
209
+ continue;
210
+ }
211
+
212
+ const bulletMatch = line.match(/^[-*]\s+(.+)$/);
213
+ if (bulletMatch && current) {
214
+ sections.get(current).push(normalizeInline(bulletMatch[1]));
215
+ bulletSections += 1;
216
+ continue;
217
+ }
218
+
219
+ if (current) {
220
+ sections.get(current).push(normalizeInline(line));
221
+ }
222
+ }
223
+
224
+ const normalizedSections = new Map(
225
+ [...sections.entries()].map(([key, values]) => [key, values.filter(Boolean).join(" ") || "None"])
226
+ );
227
+
228
+ return {
229
+ source: "text",
230
+ status: null,
231
+ sections: normalizedSections,
232
+ bulletSections,
233
+ headingSections
234
+ };
235
+ }
236
+
237
+ export function parseContractOutput(value, contract = null) {
238
+ if (value == null) {
239
+ return {
240
+ source: "empty",
241
+ status: null,
242
+ sections: new Map(),
243
+ bulletSections: 0,
244
+ headingSections: 0
245
+ };
246
+ }
247
+
248
+ if (typeof value === "string") {
249
+ const parsed = parseJsonCandidate(value);
250
+ if (parsed && typeof parsed === "object") {
251
+ return parseContractOutput(parsed.output !== undefined ? parsed.output : parsed, contract);
252
+ }
253
+
254
+ const text = stripOuterFence(value);
255
+ const rules = getContractRules(contract);
256
+ if (rules.sectionStyle === "headings") {
257
+ return parseStructuredRollup(text);
258
+ }
259
+ return parseBulletedBriefing(text);
260
+ }
261
+
262
+ if (Array.isArray(value)) {
263
+ return parseContractOutput(value.join("\n"), contract);
264
+ }
265
+
266
+ if (typeof value === "object") {
267
+ return parseObjectOutput(value, contract);
268
+ }
269
+
270
+ return parseContractOutput(String(value), contract);
271
+ }
272
+
273
+ export function scoreFieldQuality(sectionContent) {
274
+ const raw = String(sectionContent || "");
275
+ const trimmed = raw.trim();
276
+ const lower = trimmed.toLowerCase();
277
+ const findings = [];
278
+ let score = 1;
279
+
280
+ if (!trimmed || lower === "none") {
281
+ return { score: 0, confidence: "none", findings: ["Section is empty or None"] };
282
+ }
283
+
284
+ if (/\[(title|link|summary|sub)\]|<brief update>|<one-line status>/i.test(trimmed)) {
285
+ findings.push("Section contains placeholder text");
286
+ score -= 0.5;
287
+ }
288
+
289
+ const labelStripped = trimmed.replace(/^[^:]{2,40}:\s*/, "");
290
+ if (labelStripped.length < 10) {
291
+ findings.push("Section content is very short");
292
+ score -= 0.15;
293
+ }
294
+
295
+ if (/\bsorry\b|\bcannot\b|\bfailed to\b|\bdoes not support\b/i.test(lower)) {
296
+ findings.push("Section contains apology or failure language");
297
+ score -= 0.3;
298
+ }
299
+
300
+ if (/timed out|timeout/i.test(lower)) {
301
+ findings.push("Section references a timeout");
302
+ score -= 0.35;
303
+ }
304
+
305
+ if (/^(no updates|nothing to report|n\/a|tbd|todo)$/i.test(lower) || /\bno updates\b|\bnothing to report\b/i.test(lower) && labelStripped.length < 25) {
306
+ findings.push("Section contains generic filler");
307
+ score -= 0.2;
308
+ }
309
+
310
+ score = Number(Math.max(0, Math.min(1, score)).toFixed(4));
311
+ let confidence;
312
+ if (score >= 0.8) confidence = "high";
313
+ else if (score >= 0.5) confidence = "medium";
314
+ else if (score > 0) confidence = "low";
315
+ else confidence = "none";
316
+
317
+ return { score, confidence, findings };
318
+ }
319
+
320
+ export function validateOutputContract(contract, value) {
321
+ if (!contract) return null;
322
+
323
+ const parsed = parseContractOutput(value, contract);
324
+ const requiredSections = contract.requiredSections || [];
325
+ const rules = getContractRules(contract);
326
+ const findings = [];
327
+ const sectionStates = requiredSections.map((section) => {
328
+ const normalizedSection = canonicalSectionKey(section);
329
+ const content = parsed.sections.get(normalizedSection) || null;
330
+ return {
331
+ section: normalizedSection,
332
+ present: parsed.sections.has(normalizedSection),
333
+ content,
334
+ nonEmpty: content != null && normalizeText(content).length > 0,
335
+ quality: scoreFieldQuality(content)
336
+ };
337
+ });
338
+
339
+ const missingSections = sectionStates.filter((state) => !state.present).map((state) => state.section);
340
+ const emptySections = sectionStates.filter((state) => state.present && !state.nonEmpty).map((state) => state.section);
341
+
342
+ if (missingSections.length) {
343
+ findings.push(`Missing required sections: ${missingSections.join(", ")}.`);
344
+ }
345
+ if (emptySections.length) {
346
+ findings.push(`Required sections are empty: ${emptySections.join(", ")}.`);
347
+ }
348
+
349
+ const formatSignals = {
350
+ source: parsed.source,
351
+ statusPresent: Boolean(parsed.status),
352
+ bulletSections: parsed.bulletSections,
353
+ headingSections: parsed.headingSections
354
+ };
355
+
356
+ if (rules.sectionStyle === "bullets") {
357
+ if (!formatSignals.statusPresent) {
358
+ findings.push("Bulleted briefing is missing a status line.");
359
+ }
360
+ if (rules.requireSectionItems && formatSignals.source === "text" && formatSignals.bulletSections < requiredSections.length) {
361
+ findings.push("Bulleted briefing does not render each required section as its own bullet.");
362
+ }
363
+ }
364
+
365
+ if (rules.sectionStyle === "headings") {
366
+ if (formatSignals.source === "text" && formatSignals.headingSections < requiredSections.length) {
367
+ findings.push("Structured rollup does not render each required section as its own heading.");
368
+ }
369
+ if (rules.requireSectionItems && formatSignals.source === "text" && formatSignals.bulletSections < requiredSections.length) {
370
+ findings.push("Structured rollup does not provide bullet content under each required section.");
371
+ }
372
+ }
373
+
374
+ const sectionRatio =
375
+ requiredSections.length === 0
376
+ ? 1
377
+ : Math.max(0, (requiredSections.length - missingSections.length - emptySections.length) / requiredSections.length);
378
+
379
+ let formatRatio = 1;
380
+ if (rules.sectionStyle === "bullets") {
381
+ const statusRatio = rules.requireStatus ? (formatSignals.statusPresent ? 1 : 0) : 1;
382
+ const bulletRatio =
383
+ requiredSections.length === 0 ? 1 : Math.min(1, formatSignals.bulletSections / requiredSections.length);
384
+ formatRatio = formatSignals.source === "text" ? (statusRatio + bulletRatio) / 2 : statusRatio;
385
+ }
386
+
387
+ if (rules.sectionStyle === "headings") {
388
+ if (formatSignals.source === "text") {
389
+ const headingRatio =
390
+ requiredSections.length === 0 ? 1 : Math.min(1, formatSignals.headingSections / requiredSections.length);
391
+ const bulletRatio = rules.requireSectionItems
392
+ ? requiredSections.length === 0
393
+ ? 1
394
+ : Math.min(1, formatSignals.bulletSections / requiredSections.length)
395
+ : 1;
396
+ formatRatio = (headingRatio + bulletRatio) / 2;
397
+ }
398
+ }
399
+
400
+ const satisfiedRatio = Number(Math.max(0, Math.min(sectionRatio, formatRatio)).toFixed(4));
401
+
402
+ const scores = {};
403
+ const weakFields = [];
404
+ const placeholderFields = [];
405
+ for (const state of sectionStates) {
406
+ scores[state.section] = state.quality;
407
+ if (state.quality.score < 0.5) weakFields.push(state.section);
408
+ if (state.quality.findings.some((f) => /placeholder/i.test(f))) placeholderFields.push(state.section);
409
+ }
410
+ const allScores = sectionStates.map((s) => s.quality.score);
411
+ const averageScore = allScores.length
412
+ ? Number((allScores.reduce((sum, v) => sum + v, 0) / allScores.length).toFixed(4))
413
+ : 0;
414
+
415
+ const fieldScores = {
416
+ scores,
417
+ averageScore,
418
+ weakFields,
419
+ placeholderFields
420
+ };
421
+
422
+ return {
423
+ format: contract.format || null,
424
+ requiredSections,
425
+ styleHints: contract.styleHints || [],
426
+ profile: contract.profile || null,
427
+ parsed: {
428
+ status: parsed.status,
429
+ sections: Object.fromEntries(parsed.sections),
430
+ formatSignals
431
+ },
432
+ sectionStates,
433
+ satisfiedRatio: Math.max(0, satisfiedRatio),
434
+ missingSections,
435
+ emptySections,
436
+ fieldScores,
437
+ findings
438
+ };
439
+ }
@@ -0,0 +1,69 @@
1
+ import { OpenClawShadowBridge } from "../shadow/bridge.js";
2
+ import { PeerRegistry } from "./peer-registry.js";
3
+
4
+ function resolveNamedProfile(profiles, profileName) {
5
+ if (!profileName) return null;
6
+ return profiles[profileName] || profiles[String(profileName).replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())] || null;
7
+ }
8
+
9
+ export class PeerReadinessProbe {
10
+ constructor({ liveRoot } = {}) {
11
+ this.bridge = new OpenClawShadowBridge({ liveRoot });
12
+ }
13
+
14
+ async probe(runtime, peerId) {
15
+ const registry = new PeerRegistry(runtime.peers);
16
+ const peer = registry.get(peerId);
17
+ if (!peer) {
18
+ return {
19
+ peerId,
20
+ knownPeer: false,
21
+ routeResolvable: false,
22
+ targetReachable: false,
23
+ deliveryProfileValid: false,
24
+ readiness: "unknown_peer",
25
+ findings: ["Peer is not present in the trusted registry."]
26
+ };
27
+ }
28
+
29
+ const resolved = registry.resolveTarget({ mode: "peer_agent", peerId });
30
+ const deliveryProfile = resolved.deliveryProfile || runtime.delivery?.defaultPeerProfile || null;
31
+ const profileValid = Boolean(resolveNamedProfile(runtime.delivery?.profiles || {}, deliveryProfile));
32
+ const routeResolvable = Boolean(resolved.sessionKey);
33
+ let targetReachable;
34
+ const findings = [];
35
+
36
+ if (!profileValid) findings.push(`Delivery profile is missing: ${deliveryProfile || "(none)"}`);
37
+ if (!routeResolvable) findings.push("Peer route does not have a resolvable session key.");
38
+
39
+ if (this.bridge.available && peer.agentId) {
40
+ const sessionIndex = await this.bridge.loadSessionIndex(peer.agentId);
41
+ targetReachable = Boolean(resolved.sessionKey && sessionIndex?.[resolved.sessionKey]);
42
+ if (!targetReachable && routeResolvable) {
43
+ findings.push("Peer route is resolvable but not currently present in the live session index.");
44
+ }
45
+ } else {
46
+ targetReachable = routeResolvable && profileValid;
47
+ }
48
+
49
+ const readiness = !profileValid
50
+ ? "profile_invalid"
51
+ : !routeResolvable
52
+ ? "route_unresolvable"
53
+ : targetReachable
54
+ ? "ready"
55
+ : "route_only";
56
+
57
+ return {
58
+ peerId,
59
+ knownPeer: true,
60
+ routeResolvable,
61
+ targetReachable,
62
+ deliveryProfileValid: profileValid,
63
+ deliveryProfile,
64
+ sessionKey: resolved.sessionKey || null,
65
+ readiness,
66
+ findings
67
+ };
68
+ }
69
+ }
@@ -0,0 +1,133 @@
1
+ export class PeerRegistry {
2
+ constructor(config = {}) {
3
+ this.peers = config.peers || {};
4
+ }
5
+
6
+ get(peerId) {
7
+ return this.peers[peerId] || null;
8
+ }
9
+
10
+ list() {
11
+ return Object.entries(this.peers).map(([peerId, peer]) => ({
12
+ peerId,
13
+ ...peer
14
+ }));
15
+ }
16
+
17
+ listCards() {
18
+ return this.list()
19
+ .filter((peer) => peer.card)
20
+ .map((peer) => ({
21
+ peerId: peer.peerId,
22
+ agentId: peer.agentId || null,
23
+ label: peer.label || peer.peerId,
24
+ deliveryProfile: peer.deliveryProfile || null,
25
+ sessionKeys: peer.sessionKeys || [],
26
+ ...peer.card
27
+ }));
28
+ }
29
+
30
+ findByCapability(query) {
31
+ const terms = String(query || "")
32
+ .toLowerCase()
33
+ .split(/[^a-z0-9]+/i)
34
+ .filter(Boolean);
35
+ if (!terms.length) return this.listCards();
36
+
37
+ return this.listCards()
38
+ .map((card) => {
39
+ const haystack = [
40
+ card.label,
41
+ card.role,
42
+ card.mission,
43
+ ...(card.capabilityTags || []),
44
+ ...(card.preferredTaskClasses || [])
45
+ ]
46
+ .filter(Boolean)
47
+ .join(" ")
48
+ .toLowerCase();
49
+ const matches = terms.filter((term) => haystack.includes(term));
50
+ return {
51
+ ...card,
52
+ matchCount: matches.length
53
+ };
54
+ })
55
+ .filter((card) => card.matchCount > 0)
56
+ .sort((a, b) => b.matchCount - a.matchCount || a.label.localeCompare(b.label));
57
+ }
58
+
59
+ suggestPeers({ taskType = null, query = "", preferredDeliveryProfile = null, trustLevel = null, limit = 3 } = {}) {
60
+ const cards = this.listCards();
61
+ const queryTerms = String(query || "")
62
+ .toLowerCase()
63
+ .split(/[^a-z0-9]+/i)
64
+ .filter(Boolean);
65
+ const taskKey = String(taskType || "").toLowerCase().trim();
66
+
67
+ return cards
68
+ .map((card) => {
69
+ let score = 0;
70
+ const reasons = [];
71
+
72
+ if (taskKey && (card.preferredTaskClasses || []).map((item) => String(item).toLowerCase()).includes(taskKey)) {
73
+ score += 4;
74
+ reasons.push(`preferred task class:${taskKey}`);
75
+ }
76
+
77
+ const tagMatches = (card.capabilityTags || []).filter((tag) => queryTerms.includes(String(tag).toLowerCase()));
78
+ if (tagMatches.length) {
79
+ score += tagMatches.length * 3;
80
+ reasons.push(`capability tags:${tagMatches.join(",")}`);
81
+ }
82
+
83
+ const text = [card.role, card.mission, ...(card.capabilityTags || []), ...(card.preferredTaskClasses || [])]
84
+ .filter(Boolean)
85
+ .join(" ")
86
+ .toLowerCase();
87
+ const fuzzyMatches = queryTerms.filter((term) => text.includes(term) && !tagMatches.includes(term));
88
+ if (fuzzyMatches.length) {
89
+ score += fuzzyMatches.length;
90
+ reasons.push(`text match:${fuzzyMatches.join(",")}`);
91
+ }
92
+
93
+ if (preferredDeliveryProfile && (card.deliveryPreferences || []).includes(preferredDeliveryProfile)) {
94
+ score += 1;
95
+ reasons.push(`delivery:${preferredDeliveryProfile}`);
96
+ }
97
+
98
+ if (trustLevel && card.trustLevel === trustLevel) {
99
+ score += 1;
100
+ reasons.push(`trust:${trustLevel}`);
101
+ }
102
+
103
+ return {
104
+ ...card,
105
+ score,
106
+ reasons
107
+ };
108
+ })
109
+ .filter((card) => card.score > 0)
110
+ .sort((a, b) => b.score - a.score || a.label.localeCompare(b.label))
111
+ .slice(0, limit);
112
+ }
113
+
114
+ resolveTarget(target) {
115
+ if (!target || typeof target !== "object") return null;
116
+ if (target.mode !== "peer_agent") return null;
117
+
118
+ const peerId = target.peerId || target.agentId || null;
119
+ const peer = peerId ? this.get(peerId) : null;
120
+ if (!peer) {
121
+ throw new Error(`Unknown peer target ${peerId || "(missing peerId)"}`);
122
+ }
123
+
124
+ return {
125
+ peerId,
126
+ peer,
127
+ sessionKey: target.sessionKey || peer.sessionKeys?.[0] || null,
128
+ deliveryProfile: target.deliveryProfile || peer.deliveryProfile || null,
129
+ label: peer.label || peerId,
130
+ card: peer.card || null
131
+ };
132
+ }
133
+ }