@cohortapp/agent-sdk 2.3.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 (731) hide show
  1. package/.claude/commands/init-agent.md +104 -0
  2. package/.claude/commands/init-maestro.md +1187 -0
  3. package/.claude/settings.json +161 -0
  4. package/.env.example +216 -0
  5. package/README.md +632 -0
  6. package/agents/browser-operator/agent.md +52 -0
  7. package/agents/calendar-ops/agent.md +50 -0
  8. package/agents/communications/agent.md +96 -0
  9. package/agents/decision-log/agent.md +65 -0
  10. package/agents/desktop-operator/agent.md +59 -0
  11. package/agents/gmail-operator/agent.md +62 -0
  12. package/agents/inbound-dispatcher/agent.md +66 -0
  13. package/agents/inbox-processor/agent.md +39 -0
  14. package/agents/pmo-execution/agent.md +60 -0
  15. package/agents/session-spawner/agent.md +64 -0
  16. package/agents/slack-operator/agent.md +60 -0
  17. package/agents/whatsapp-operator/agent.md +60 -0
  18. package/agents/workflow-automation/agent.md +61 -0
  19. package/archetypes/altitudes/c-suite.yaml +58 -0
  20. package/archetypes/altitudes/founder.yaml +68 -0
  21. package/archetypes/altitudes/senior-manager.yaml +63 -0
  22. package/archetypes/altitudes/svp.yaml +60 -0
  23. package/archetypes/altitudes/vp.yaml +50 -0
  24. package/archetypes/archetype.schema.json +77 -0
  25. package/archetypes/base.yaml +47 -0
  26. package/archetypes/capabilities/commercial-leader.yaml +159 -0
  27. package/archetypes/capabilities/compliance-officer.yaml +159 -0
  28. package/archetypes/capabilities/executive-operator.yaml +169 -0
  29. package/archetypes/capabilities/finance-leader.yaml +162 -0
  30. package/archetypes/capabilities/operations-leader.yaml +154 -0
  31. package/archetypes/capabilities/product-leader.yaml +148 -0
  32. package/archetypes/capabilities/technical-leader.yaml +146 -0
  33. package/archetypes/functions/commercial-leader.yaml +62 -0
  34. package/archetypes/functions/compliance-officer.yaml +64 -0
  35. package/archetypes/functions/executive-operator.yaml +70 -0
  36. package/archetypes/functions/finance-leader.yaml +70 -0
  37. package/archetypes/functions/operations-leader.yaml +62 -0
  38. package/archetypes/functions/product-leader.yaml +61 -0
  39. package/archetypes/functions/technical-leader.yaml +57 -0
  40. package/bin/cohort-mcp.mjs +81 -0
  41. package/bin/maestro.mjs +3516 -0
  42. package/bin/maestro.test.mjs +1015 -0
  43. package/desktop-control/README.md +56 -0
  44. package/desktop-control/app-profiles/gmail.yaml +120 -0
  45. package/desktop-control/app-profiles/slack.yaml +315 -0
  46. package/desktop-control/app-profiles/whatsapp.yaml +107 -0
  47. package/docs/architecture/agent-topology.md +2239 -0
  48. package/docs/architecture/archetype-agent-factory.md +110 -0
  49. package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
  50. package/docs/architecture/continuous-monitoring.md +221 -0
  51. package/docs/architecture/mcp-capability-map.md +585 -0
  52. package/docs/architecture/system-architecture.md +1272 -0
  53. package/docs/company-context/README.md +40 -0
  54. package/docs/guides/agent-persona-setup.md +600 -0
  55. package/docs/guides/agents-observe-setup.md +64 -0
  56. package/docs/guides/billing-console-keys.md +88 -0
  57. package/docs/guides/ccxray-diagnostics.md +65 -0
  58. package/docs/guides/channel-bus.md +127 -0
  59. package/docs/guides/claude-mem-setup.md +79 -0
  60. package/docs/guides/claude-pace-setup.md +56 -0
  61. package/docs/guides/claudraband-sessions.md +98 -0
  62. package/docs/guides/clawteam-swarm.md +116 -0
  63. package/docs/guides/code-review-graph-setup.md +86 -0
  64. package/docs/guides/email-setup.md +431 -0
  65. package/docs/guides/mac-mini.md +119 -0
  66. package/docs/guides/media-generation-setup.md +349 -0
  67. package/docs/guides/model-routing.md +162 -0
  68. package/docs/guides/observability-otel.md +265 -0
  69. package/docs/guides/org-onboarding.md +132 -0
  70. package/docs/guides/outbound-governance-setup.md +437 -0
  71. package/docs/guides/pdf-generation-setup.md +315 -0
  72. package/docs/guides/poller-daemon-setup.md +563 -0
  73. package/docs/guides/rag-context-setup.md +459 -0
  74. package/docs/guides/self-optimization-pattern.md +82 -0
  75. package/docs/guides/setup-wizard.md +178 -0
  76. package/docs/guides/slack-setup.md +350 -0
  77. package/docs/guides/telegram-setup.md +227 -0
  78. package/docs/guides/twilio-subaccounts-setup.md +223 -0
  79. package/docs/guides/verification.md +128 -0
  80. package/docs/guides/voice-mode.md +188 -0
  81. package/docs/guides/voice-sms-setup.md +698 -0
  82. package/docs/guides/webhook-relay-setup.md +349 -0
  83. package/docs/guides/whatsapp-setup.md +288 -0
  84. package/docs/prompts/board-pack-cover-template.md +36 -0
  85. package/docs/prompts/decision-recommendation-template.md +88 -0
  86. package/docs/prompts/followup-message-template.md +141 -0
  87. package/docs/prompts/investor-letter-template.md +52 -0
  88. package/docs/prompts/morning-brief-template.md +82 -0
  89. package/docs/prompts/presentation-template.md +58 -0
  90. package/docs/prompts/weekly-strategic-memo-template.md +104 -0
  91. package/docs/research/hallucinated-tool-output-investigation.md +151 -0
  92. package/docs/runbooks/backup-restore.md +205 -0
  93. package/docs/runbooks/cohort-cutover.md +129 -0
  94. package/docs/runbooks/fleet-operations.md +200 -0
  95. package/docs/runbooks/incident-response.md +226 -0
  96. package/docs/runbooks/mac-mini-bootstrap.md +431 -0
  97. package/docs/runbooks/perpetual-operations.md +509 -0
  98. package/docs/runbooks/recovery-and-failover.md +260 -0
  99. package/framework-features.json +267 -0
  100. package/ingest/README.md +87 -0
  101. package/lib/action-executor.js +689 -0
  102. package/lib/action-executor.test.mjs +871 -0
  103. package/lib/agent-root.mjs +37 -0
  104. package/lib/archetype.mjs +236 -0
  105. package/lib/archetype.test.mjs +132 -0
  106. package/lib/autonomy.mjs +114 -0
  107. package/lib/autonomy.test.mjs +66 -0
  108. package/lib/backlog.mjs +358 -0
  109. package/lib/backlog.test.mjs +266 -0
  110. package/lib/budget-guard.mjs +279 -0
  111. package/lib/budget-guard.test.mjs +291 -0
  112. package/lib/cadence-bus-schedule.test.mjs +194 -0
  113. package/lib/cadence-bus.mjs +1120 -0
  114. package/lib/cadence-bus.test.mjs +720 -0
  115. package/lib/cadences.mjs +205 -0
  116. package/lib/cadences.test.mjs +125 -0
  117. package/lib/capability.mjs +154 -0
  118. package/lib/capability.test.mjs +78 -0
  119. package/lib/channels/base-adapter.mjs +719 -0
  120. package/lib/channels/base-adapter.test.mjs +590 -0
  121. package/lib/channels/channel.mjs +128 -0
  122. package/lib/channels/channels.test.mjs +371 -0
  123. package/lib/channels/contract.mjs +215 -0
  124. package/lib/channels/contract.test.mjs +137 -0
  125. package/lib/channels/conversation-resolver.mjs +95 -0
  126. package/lib/channels/gmail/adapter.mjs +87 -0
  127. package/lib/channels/inbox-item.mjs +255 -0
  128. package/lib/channels/inbox-item.test.mjs +335 -0
  129. package/lib/channels/index.mjs +94 -0
  130. package/lib/channels/orgmail/adapter.mjs +353 -0
  131. package/lib/channels/orgmail/adapter.test.mjs +311 -0
  132. package/lib/channels/pairing.mjs +363 -0
  133. package/lib/channels/pairing.test.mjs +270 -0
  134. package/lib/channels/registry.mjs +164 -0
  135. package/lib/channels/slack/adapter.mjs +317 -0
  136. package/lib/channels/slack-adapter.test.mjs +212 -0
  137. package/lib/channels/sms/adapter.mjs +43 -0
  138. package/lib/channels/telegram/adapter.mjs +432 -0
  139. package/lib/channels/telegram-adapter.test.mjs +306 -0
  140. package/lib/channels/voice/adapter.mjs +301 -0
  141. package/lib/channels/voice/adapter.test.mjs +278 -0
  142. package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
  143. package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
  144. package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
  145. package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
  146. package/lib/charter.mjs +256 -0
  147. package/lib/charter.test.mjs +89 -0
  148. package/lib/claude-bin.mjs +134 -0
  149. package/lib/claude-bin.test.mjs +75 -0
  150. package/lib/collective/capture.mjs +185 -0
  151. package/lib/collective/capture.test.mjs +121 -0
  152. package/lib/collective/cards.mjs +201 -0
  153. package/lib/collective/cards.test.mjs +114 -0
  154. package/lib/collective/config.mjs +186 -0
  155. package/lib/collective/config.test.mjs +123 -0
  156. package/lib/collective/global-config.mjs +113 -0
  157. package/lib/collective/global-config.test.mjs +75 -0
  158. package/lib/collective/presence.mjs +201 -0
  159. package/lib/collective/presence.test.mjs +95 -0
  160. package/lib/collective/recall.mjs +215 -0
  161. package/lib/collective/recall.test.mjs +116 -0
  162. package/lib/comms/send-gate.mjs +554 -0
  163. package/lib/comms/send-gate.test.mjs +577 -0
  164. package/lib/comms.mjs +67 -0
  165. package/lib/comms.test.mjs +41 -0
  166. package/lib/diagnostics/alerts.mjs +424 -0
  167. package/lib/diagnostics/alerts.test.mjs +318 -0
  168. package/lib/diagnostics/backup-freshness.mjs +188 -0
  169. package/lib/diagnostics/backup-freshness.test.mjs +185 -0
  170. package/lib/diagnostics/counters.mjs +269 -0
  171. package/lib/diagnostics/counters.test.mjs +206 -0
  172. package/lib/diagnostics/events.mjs +188 -0
  173. package/lib/diagnostics/events.test.mjs +290 -0
  174. package/lib/diagnostics/otel.mjs +237 -0
  175. package/lib/diagnostics/otel.test.mjs +196 -0
  176. package/lib/diagnostics/trace.mjs +216 -0
  177. package/lib/diagnostics/trace.test.mjs +251 -0
  178. package/lib/env-compat.mjs +74 -0
  179. package/lib/env-compat.test.mjs +104 -0
  180. package/lib/feature-init.mjs +331 -0
  181. package/lib/fs-atomic.mjs +112 -0
  182. package/lib/fs-atomic.test.mjs +72 -0
  183. package/lib/fs-ownership.mjs +111 -0
  184. package/lib/fs-ownership.test.mjs +158 -0
  185. package/lib/hooks/bus.mjs +347 -0
  186. package/lib/hooks/bus.test.mjs +387 -0
  187. package/lib/index.js +16 -0
  188. package/lib/learning/config.mjs +106 -0
  189. package/lib/learning/config.test.mjs +75 -0
  190. package/lib/learning/counters.mjs +156 -0
  191. package/lib/learning/counters.test.mjs +69 -0
  192. package/lib/learning/curator-consolidate.test.mjs +238 -0
  193. package/lib/learning/curator.mjs +453 -0
  194. package/lib/learning/curator.test.mjs +106 -0
  195. package/lib/learning/log.mjs +40 -0
  196. package/lib/learning/reflect.mjs +534 -0
  197. package/lib/learning/reflect.test.mjs +0 -0
  198. package/lib/learning/session-index.mjs +352 -0
  199. package/lib/learning/session-index.test.mjs +125 -0
  200. package/lib/learning/skill-writer.mjs +474 -0
  201. package/lib/learning/skill-writer.test.mjs +210 -0
  202. package/lib/mcp/server.mjs +328 -0
  203. package/lib/mcp/server.test.mjs +400 -0
  204. package/lib/model-router/auth-profiles.mjs +758 -0
  205. package/lib/model-router/auth-profiles.test.mjs +580 -0
  206. package/lib/model-router/catalog/anthropic.yaml +153 -0
  207. package/lib/model-router/catalog/deepseek.yaml +86 -0
  208. package/lib/model-router/catalog/moonshot.yaml +81 -0
  209. package/lib/model-router/catalog/qwen.yaml +114 -0
  210. package/lib/model-router/catalog.mjs +925 -0
  211. package/lib/model-router/catalog.test.mjs +385 -0
  212. package/lib/model-router/economics.mjs +564 -0
  213. package/lib/model-router/economics.test.mjs +344 -0
  214. package/lib/model-router/failover.mjs +298 -0
  215. package/lib/model-router/failover.test.mjs +439 -0
  216. package/lib/model-router/health.mjs +453 -0
  217. package/lib/model-router/health.test.mjs +338 -0
  218. package/lib/model-router/integration-coverage.test.mjs +829 -0
  219. package/lib/model-router/integration.test.mjs +564 -0
  220. package/lib/model-router/ledger.mjs +402 -0
  221. package/lib/model-router/ledger.test.mjs +382 -0
  222. package/lib/model-router/llm-task.mjs +515 -0
  223. package/lib/model-router/llm-task.test.mjs +392 -0
  224. package/lib/model-router/org-credentials.mjs +260 -0
  225. package/lib/model-router/org-credentials.test.mjs +265 -0
  226. package/lib/model-router/pricing-refresh.mjs +463 -0
  227. package/lib/model-router/pricing-refresh.test.mjs +286 -0
  228. package/lib/model-router/reconcile.mjs +429 -0
  229. package/lib/model-router/reconcile.test.mjs +316 -0
  230. package/lib/model-router/repair.mjs +471 -0
  231. package/lib/model-router/repair.test.mjs +180 -0
  232. package/lib/model-router/resolve.mjs +1206 -0
  233. package/lib/model-router/spawn.mjs +497 -0
  234. package/lib/model-router/spawn.test.mjs +425 -0
  235. package/lib/model-router/taxonomy.mjs +893 -0
  236. package/lib/model-router/taxonomy.test.mjs +410 -0
  237. package/lib/model-router.mjs +677 -0
  238. package/lib/model-router.test.mjs +907 -0
  239. package/lib/org/activity.mjs +211 -0
  240. package/lib/org/activity.test.mjs +134 -0
  241. package/lib/org/approvals.mjs +448 -0
  242. package/lib/org/approvals.test.mjs +216 -0
  243. package/lib/org/awareness.mjs +222 -0
  244. package/lib/org/awareness.test.mjs +159 -0
  245. package/lib/org/board.mjs +229 -0
  246. package/lib/org/board.test.mjs +177 -0
  247. package/lib/org/bootstrap-context.mjs +169 -0
  248. package/lib/org/bootstrap-context.test.mjs +153 -0
  249. package/lib/org/client.mjs +1628 -0
  250. package/lib/org/client.test.mjs +1107 -0
  251. package/lib/org/cohort-client.mjs +67 -0
  252. package/lib/org/cohort-client.test.mjs +126 -0
  253. package/lib/org/cost-sync.mjs +227 -0
  254. package/lib/org/cost-sync.test.mjs +153 -0
  255. package/lib/org/doctor.mjs +212 -0
  256. package/lib/org/doctor.test.mjs +212 -0
  257. package/lib/org/handoff.mjs +293 -0
  258. package/lib/org/handoff.test.mjs +269 -0
  259. package/lib/org/integration-tools.mjs +182 -0
  260. package/lib/org/integration-tools.test.mjs +160 -0
  261. package/lib/org/keys.mjs +131 -0
  262. package/lib/org/keys.test.mjs +92 -0
  263. package/lib/org/knowledge.mjs +463 -0
  264. package/lib/org/knowledge.test.mjs +319 -0
  265. package/lib/org/leases.mjs +335 -0
  266. package/lib/org/leases.test.mjs +235 -0
  267. package/lib/org/mesh-integration.test.mjs +127 -0
  268. package/lib/org/mesh.mjs +459 -0
  269. package/lib/org/mesh.test.mjs +345 -0
  270. package/lib/org/messaging.mjs +503 -0
  271. package/lib/org/messaging.test.mjs +238 -0
  272. package/lib/org/policy.mjs +345 -0
  273. package/lib/org/policy.test.mjs +237 -0
  274. package/lib/org/protocol.checksum +1 -0
  275. package/lib/org/protocol.checksum.test.mjs +90 -0
  276. package/lib/org/protocol.mjs +967 -0
  277. package/lib/org/protocol.test.mjs +264 -0
  278. package/lib/org/registry.mjs +194 -0
  279. package/lib/org/registry.test.mjs +100 -0
  280. package/lib/org/tool-surface-integration.test.mjs +120 -0
  281. package/lib/org/tool-surface.mjs +2535 -0
  282. package/lib/org/tool-surface.test.mjs +589 -0
  283. package/lib/org/ui-parity.mjs +3236 -0
  284. package/lib/org/ui-parity.test.mjs +348 -0
  285. package/lib/org/verify.mjs +176 -0
  286. package/lib/org/verify.test.mjs +194 -0
  287. package/lib/rag/embed.mjs +188 -0
  288. package/lib/rag/indexer.mjs +425 -0
  289. package/lib/rag/rag.test.mjs +505 -0
  290. package/lib/rag/search.mjs +475 -0
  291. package/lib/rate-guard.mjs +246 -0
  292. package/lib/rate-guard.test.mjs +201 -0
  293. package/lib/render.mjs +112 -0
  294. package/lib/render.test.mjs +68 -0
  295. package/lib/resource-governor.mjs +297 -0
  296. package/lib/resource-governor.test.mjs +262 -0
  297. package/lib/scheduling/dynamic-jobs.mjs +675 -0
  298. package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
  299. package/lib/scheduling/jitter.mjs +0 -0
  300. package/lib/scheduling/jitter.test.mjs +140 -0
  301. package/lib/secrets/broker.mjs +315 -0
  302. package/lib/secrets/broker.test.mjs +280 -0
  303. package/lib/secrets/providers.mjs +461 -0
  304. package/lib/secrets/providers.test.mjs +274 -0
  305. package/lib/security/audit-engine.mjs +684 -0
  306. package/lib/security/audit-engine.test.mjs +389 -0
  307. package/lib/security/coerce-args.mjs +552 -0
  308. package/lib/security/coerce-args.test.mjs +281 -0
  309. package/lib/security/dangerous-tools.mjs +97 -0
  310. package/lib/security/dangerous-tools.test.mjs +68 -0
  311. package/lib/security/external-content.mjs +145 -0
  312. package/lib/security/external-content.test.mjs +67 -0
  313. package/lib/security/redact.mjs +592 -0
  314. package/lib/security/redact.test.mjs +441 -0
  315. package/lib/security/secret-equal.mjs +73 -0
  316. package/lib/security/secret-equal.test.mjs +55 -0
  317. package/lib/session-permissions.mjs +101 -0
  318. package/lib/session-permissions.test.mjs +100 -0
  319. package/lib/setup/claude-probe.mjs +74 -0
  320. package/lib/setup/completeness.mjs +175 -0
  321. package/lib/setup/completeness.test.mjs +110 -0
  322. package/lib/setup/context-pack.mjs +173 -0
  323. package/lib/setup/context-pack.test.mjs +89 -0
  324. package/lib/setup/enrich.mjs +277 -0
  325. package/lib/setup/enrich.test.mjs +115 -0
  326. package/lib/setup/enroll-from-cohort.mjs +441 -0
  327. package/lib/setup/enroll-from-cohort.test.mjs +233 -0
  328. package/lib/setup/integration.test.mjs +162 -0
  329. package/lib/setup/io.mjs +360 -0
  330. package/lib/setup/io.test.mjs +77 -0
  331. package/lib/setup/run-generator.mjs +81 -0
  332. package/lib/setup/runner.mjs +244 -0
  333. package/lib/setup/runner.test.mjs +132 -0
  334. package/lib/setup/sections/comms.mjs +173 -0
  335. package/lib/setup/sections/company.mjs +120 -0
  336. package/lib/setup/sections/enrich.mjs +138 -0
  337. package/lib/setup/sections/identity.mjs +182 -0
  338. package/lib/setup/sections/identity.test.mjs +140 -0
  339. package/lib/setup/sections/learning.mjs +153 -0
  340. package/lib/setup/sections/learning.test.mjs +81 -0
  341. package/lib/setup/sections/messaging.mjs +219 -0
  342. package/lib/setup/sections/messaging.test.mjs +127 -0
  343. package/lib/setup/sections/model.mjs +102 -0
  344. package/lib/setup/sections/operating-model.mjs +78 -0
  345. package/lib/setup/sections/org.mjs +475 -0
  346. package/lib/setup/sections/org.test.mjs +313 -0
  347. package/lib/setup/sections/orgmail.mjs +173 -0
  348. package/lib/setup/sections/orgmail.test.mjs +118 -0
  349. package/lib/setup/sections/recovery.mjs +159 -0
  350. package/lib/setup/sections/recovery.test.mjs +98 -0
  351. package/lib/setup/sections/tools.mjs +132 -0
  352. package/lib/setup/sections/verify.mjs +97 -0
  353. package/lib/setup/sot.mjs +205 -0
  354. package/lib/setup/sot.test.mjs +81 -0
  355. package/lib/setup/state.mjs +151 -0
  356. package/lib/setup/state.test.mjs +92 -0
  357. package/lib/singleton.js +229 -0
  358. package/lib/singleton.test.mjs +135 -0
  359. package/lib/telemetry/alerts.mjs +216 -0
  360. package/lib/telemetry/alerts.test.mjs +109 -0
  361. package/lib/telemetry/collect.mjs +512 -0
  362. package/lib/telemetry/collect.test.mjs +202 -0
  363. package/lib/tool-definitions-integration.test.mjs +83 -0
  364. package/lib/tool-definitions.js +738 -0
  365. package/lib/tool-definitions.test.mjs +437 -0
  366. package/lib/util/fetch-timeout.mjs +136 -0
  367. package/lib/util/fetch-timeout.test.mjs +202 -0
  368. package/lib/util/reconnect.mjs +343 -0
  369. package/lib/util/reconnect.test.mjs +369 -0
  370. package/lib/util/unhandled.mjs +205 -0
  371. package/lib/util/unhandled.test.mjs +216 -0
  372. package/lib/voice/context-loader.mjs +466 -0
  373. package/lib/voice/index.mjs +100 -0
  374. package/lib/voice/openai-realtime.mjs +510 -0
  375. package/lib/voice/outbound.mjs +542 -0
  376. package/lib/voice/outbound.test.mjs +69 -0
  377. package/lib/voice/post-call-brief.mjs +428 -0
  378. package/lib/voice/provider.mjs +52 -0
  379. package/lib/voice/session-rotation.mjs +257 -0
  380. package/lib/voice/stt.mjs +161 -0
  381. package/lib/voice/stt.test.mjs +226 -0
  382. package/lib/voice/tool-bridge.mjs +370 -0
  383. package/lib/voice/tts.mjs +104 -0
  384. package/lib/voice/twilio-sip-bridge.mjs +288 -0
  385. package/lib/voice/voice.test.mjs +990 -0
  386. package/mcp/README.md +80 -0
  387. package/package.json +151 -0
  388. package/plugins/maestro-skills/plugin.json +139 -0
  389. package/plugins/maestro-skills/skills/agents-observe.md +110 -0
  390. package/plugins/maestro-skills/skills/board-deck.md +68 -0
  391. package/plugins/maestro-skills/skills/books-close.md +77 -0
  392. package/plugins/maestro-skills/skills/brand-steward.md +121 -0
  393. package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
  394. package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
  395. package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
  396. package/plugins/maestro-skills/skills/claude-pace.md +61 -0
  397. package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
  398. package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
  399. package/plugins/maestro-skills/skills/decision-brief.md +89 -0
  400. package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
  401. package/plugins/maestro-skills/skills/draft-comms.md +84 -0
  402. package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
  403. package/plugins/maestro-skills/skills/files-find.md +65 -0
  404. package/plugins/maestro-skills/skills/generative-ui.md +228 -0
  405. package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
  406. package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
  407. package/plugins/maestro-skills/skills/mail-triage.md +86 -0
  408. package/plugins/maestro-skills/skills/morning-brief.md +54 -0
  409. package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
  410. package/plugins/maestro-skills/skills/org-board.md +133 -0
  411. package/plugins/maestro-skills/skills/org-credential.md +68 -0
  412. package/plugins/maestro-skills/skills/org-recall.md +81 -0
  413. package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
  414. package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
  415. package/plugins/maestro-skills/skills/router-why.md +78 -0
  416. package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
  417. package/plugins/maestro-skills/skills/session-search.md +71 -0
  418. package/plugins/maestro-skills/skills/set-reminder.md +93 -0
  419. package/plugins/maestro-skills/skills/slack-followup.md +64 -0
  420. package/plugins/maestro-skills/skills/team-activity.md +86 -0
  421. package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
  422. package/policies/action-classification.yaml +114 -0
  423. package/policies/ai-disclosure.yaml +294 -0
  424. package/policies/communication-style.md +139 -0
  425. package/policies/information-barriers.yaml +118 -0
  426. package/policies/prompt-injection-defence.yaml +138 -0
  427. package/public/assets/icon-dark.png +0 -0
  428. package/public/assets/icon-dark.svg +9 -0
  429. package/public/assets/icon-light.svg +9 -0
  430. package/public/assets/logo-dark.svg +15 -0
  431. package/public/assets/logo-light.svg +15 -0
  432. package/scaffold/.mcp.json +7 -0
  433. package/scaffold/CLAUDE.md +368 -0
  434. package/scaffold/config/agent.json +55 -0
  435. package/scaffold/config/agent.ts +76 -0
  436. package/scaffold/config/agent.ts.example +89 -0
  437. package/scaffold/config/alerts.yaml +23 -0
  438. package/scaffold/config/allowlist.yaml.example +25 -0
  439. package/scaffold/config/caller-id-map.yaml +46 -0
  440. package/scaffold/config/collective.yaml +49 -0
  441. package/scaffold/config/company.json +20 -0
  442. package/scaffold/config/known-agents.json +6 -0
  443. package/scaffold/config/learning.yaml +55 -0
  444. package/scaffold/config/model-routing.yaml.example +104 -0
  445. package/scaffold/config/org.yaml +25 -0
  446. package/scaffold/config/orgmail.yaml.example +19 -0
  447. package/scaffold/config/recovery.yaml +72 -0
  448. package/scaffold/config/secrets.yaml +27 -0
  449. package/scaffold/config/slack.yaml.example +35 -0
  450. package/scaffold/config/telegram.yaml.example +38 -0
  451. package/scaffold/config/voice.yaml.example +89 -0
  452. package/scaffold/config/whatsapp.yaml.example +39 -0
  453. package/schedules/README.md +49 -0
  454. package/schedules/triggers/backlog-executor.md +102 -0
  455. package/schedules/triggers/brand-steward.md +72 -0
  456. package/schedules/triggers/daily-evening-wrap.md +159 -0
  457. package/schedules/triggers/daily-midday-sweep.md +58 -0
  458. package/schedules/triggers/daily-morning-brief.md +55 -0
  459. package/schedules/triggers/directory-hygiene.md +81 -0
  460. package/schedules/triggers/dynamic-jobs.md +40 -0
  461. package/schedules/triggers/inbox-processor.md +115 -0
  462. package/schedules/triggers/meeting-action-capture.md +60 -0
  463. package/schedules/triggers/meeting-prep.md +69 -0
  464. package/schedules/triggers/messaging-inbound.md +50 -0
  465. package/schedules/triggers/org-pulse.md +24 -0
  466. package/schedules/triggers/quarterly-self-assessment.md +54 -0
  467. package/schedules/triggers/weekly-engineering-health.md +37 -0
  468. package/schedules/triggers/weekly-execution.md +65 -0
  469. package/schedules/triggers/weekly-hiring.md +53 -0
  470. package/schedules/triggers/weekly-priorities.md +38 -0
  471. package/schedules/triggers/weekly-strategic-memo.md +124 -0
  472. package/scripts/archive-email.sh +55 -0
  473. package/scripts/cadence/cadence-status.mjs +36 -0
  474. package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
  475. package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
  476. package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
  477. package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
  478. package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
  479. package/scripts/ci/check-docs-accuracy.mjs +493 -0
  480. package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
  481. package/scripts/ci/check-exports-exist.mjs +140 -0
  482. package/scripts/ci/check-files-exist.mjs +107 -0
  483. package/scripts/ci/check-no-build-artifacts.mjs +111 -0
  484. package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
  485. package/scripts/ci/check-no-confidential.mjs +198 -0
  486. package/scripts/ci/check-no-conflict-markers.mjs +169 -0
  487. package/scripts/ci/check-no-residual-identity.mjs +163 -0
  488. package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
  489. package/scripts/ci/check-tarball-fidelity.mjs +205 -0
  490. package/scripts/ci/check-unresolved-tokens.mjs +83 -0
  491. package/scripts/ci/check.mjs +109 -0
  492. package/scripts/ci/check.test.mjs +194 -0
  493. package/scripts/ci/run-coverage.mjs +82 -0
  494. package/scripts/ci/run-tests.mjs +71 -0
  495. package/scripts/cloud-relay/README.md +59 -0
  496. package/scripts/cloud-relay/index.mjs +233 -0
  497. package/scripts/cloud-relay/package.json +15 -0
  498. package/scripts/cloud-relay/railway.json +13 -0
  499. package/scripts/cloud-relay/voice/README.md +94 -0
  500. package/scripts/cloud-relay/voice/package-lock.json +39 -0
  501. package/scripts/cloud-relay/voice/package.json +16 -0
  502. package/scripts/cloud-relay/voice/railway.json +13 -0
  503. package/scripts/cloud-relay/voice/server.mjs +532 -0
  504. package/scripts/collective/hook-runner.mjs +211 -0
  505. package/scripts/collective/hook-runner.test.mjs +90 -0
  506. package/scripts/collective/org-pulse.mjs +72 -0
  507. package/scripts/collective/org-sync.mjs +61 -0
  508. package/scripts/collective/recall.mjs +45 -0
  509. package/scripts/collective/who.mjs +30 -0
  510. package/scripts/comms-monitor.sh +288 -0
  511. package/scripts/configure-whatsapp-sandbox.sh +201 -0
  512. package/scripts/continuous-monitor.sh +91 -0
  513. package/scripts/cost/fleet-digest.mjs +407 -0
  514. package/scripts/cost/fleet-digest.test.mjs +207 -0
  515. package/scripts/cost/track-claude-usage.mjs +169 -0
  516. package/scripts/daemon/agent-daemon.mjs +989 -0
  517. package/scripts/daemon/agent-daemon.test.mjs +525 -0
  518. package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
  519. package/scripts/daemon/cadence-consumer.mjs +1080 -0
  520. package/scripts/daemon/cadence-consumer.test.mjs +770 -0
  521. package/scripts/daemon/cadence-handlers.mjs +1121 -0
  522. package/scripts/daemon/cadence-handlers.test.mjs +617 -0
  523. package/scripts/daemon/classifier.mjs +704 -0
  524. package/scripts/daemon/classifier.test.mjs +238 -0
  525. package/scripts/daemon/classify-kind.mjs +54 -0
  526. package/scripts/daemon/classify-kind.test.mjs +40 -0
  527. package/scripts/daemon/context-compiler.mjs +605 -0
  528. package/scripts/daemon/context-compiler.test.mjs +300 -0
  529. package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
  530. package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
  531. package/scripts/daemon/dispatcher.mjs +1516 -0
  532. package/scripts/daemon/health.mjs +72 -0
  533. package/scripts/daemon/inbox-deferral.mjs +210 -0
  534. package/scripts/daemon/inbox-deferral.test.mjs +242 -0
  535. package/scripts/daemon/integration.test.mjs +149 -0
  536. package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
  537. package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
  538. package/scripts/daemon/launchd-wrapper.sh +91 -0
  539. package/scripts/daemon/lib/session-router.mjs +274 -0
  540. package/scripts/daemon/lib/session-router.test.mjs +295 -0
  541. package/scripts/daemon/maestro-daemon.mjs +275 -0
  542. package/scripts/daemon/prompt-builder.mjs +685 -0
  543. package/scripts/daemon/prompt-builder.test.mjs +213 -0
  544. package/scripts/daemon/responder.mjs +854 -0
  545. package/scripts/daemon/session-lock.mjs +721 -0
  546. package/scripts/daemon/session-lock.test.mjs +252 -0
  547. package/scripts/daemon/session-outcomes.mjs +640 -0
  548. package/scripts/daemon/session-outcomes.test.mjs +533 -0
  549. package/scripts/daemon/typing-registry.mjs +90 -0
  550. package/scripts/daemon/typing-registry.test.mjs +77 -0
  551. package/scripts/daemon/voice-webhook-server.mjs +804 -0
  552. package/scripts/decisions/capture-decision.mjs +116 -0
  553. package/scripts/disclosure_assessment.py +873 -0
  554. package/scripts/disclosure_boundaries.py +562 -0
  555. package/scripts/email-signature-principal.html +52 -0
  556. package/scripts/email-signature.html +60 -0
  557. package/scripts/email_quote_thread.py +167 -0
  558. package/scripts/email_thread_dedup.py +362 -0
  559. package/scripts/emergency-stop.sh +81 -0
  560. package/scripts/healthcheck.sh +116 -0
  561. package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
  562. package/scripts/hooks/block-mcp-slack-send.sh +7 -0
  563. package/scripts/hooks/post-action-log.sh +126 -0
  564. package/scripts/hooks/pre-send-audit.sh +174 -0
  565. package/scripts/hooks/pre-send-audit.test.mjs +215 -0
  566. package/scripts/hooks/session-end-log.sh +27 -0
  567. package/scripts/hooks/session-start-banner.sh +115 -0
  568. package/scripts/huddle/audio-bridge.mjs +664 -0
  569. package/scripts/huddle/boot-slack-cdp.sh +102 -0
  570. package/scripts/huddle/huddle-controller.mjs +942 -0
  571. package/scripts/huddle/huddle-server.mjs +1229 -0
  572. package/scripts/huddle/launch-slack.sh +232 -0
  573. package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
  574. package/scripts/huddle/package-lock.json +62 -0
  575. package/scripts/huddle/package.json +22 -0
  576. package/scripts/huddle/setup-audio.sh +239 -0
  577. package/scripts/huddle/start-call.mjs +318 -0
  578. package/scripts/huddle/test-pipeline.mjs +263 -0
  579. package/scripts/learning/consolidate-skills.mjs +72 -0
  580. package/scripts/learning/session-search.mjs +125 -0
  581. package/scripts/llm_email_dedup.py +442 -0
  582. package/scripts/local-triggers/generate-plists.sh +432 -0
  583. package/scripts/local-triggers/generate-plists.test.mjs +413 -0
  584. package/scripts/local-triggers/install-all.sh +49 -0
  585. package/scripts/local-triggers/plists/.gitkeep +0 -0
  586. package/scripts/local-triggers/run-trigger.sh +63 -0
  587. package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
  588. package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
  589. package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
  590. package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
  591. package/scripts/maintenance/backup-to-cloud.sh +124 -0
  592. package/scripts/maintenance/health-check.sh +377 -0
  593. package/scripts/media-generation/README.md +105 -0
  594. package/scripts/media-generation/gemini-image-client.mjs +173 -0
  595. package/scripts/media-generation/generate-assets.mjs +289 -0
  596. package/scripts/media-generation/veo-video-client.mjs +219 -0
  597. package/scripts/org/send-orgmail.mjs +227 -0
  598. package/scripts/outbound-dedup-cleanup.sh +43 -0
  599. package/scripts/outbound-dedup.sh +477 -0
  600. package/scripts/outbound_dedup.py +115 -0
  601. package/scripts/parse-voice-transcript.mjs +481 -0
  602. package/scripts/pdf-generation/README.md +63 -0
  603. package/scripts/pdf-generation/build-document.mjs +247 -0
  604. package/scripts/pdf-generation/templates/board-pack.latex +136 -0
  605. package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
  606. package/scripts/pdf-generation/templates/memo.latex +114 -0
  607. package/scripts/poll-slack-events.sh +35 -0
  608. package/scripts/poller/calendar-poller.mjs +12 -0
  609. package/scripts/poller/gmail-poller.mjs +192 -0
  610. package/scripts/poller/imap-client.mjs +289 -0
  611. package/scripts/poller/inbox-scan-poller.mjs +156 -0
  612. package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
  613. package/scripts/poller/index.mjs +73 -0
  614. package/scripts/poller/intra-session-check.mjs +285 -0
  615. package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
  616. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
  617. package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
  618. package/scripts/poller/secondary-gmail-poller.mjs +132 -0
  619. package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
  620. package/scripts/poller/slack-poller.mjs +854 -0
  621. package/scripts/poller/slack-socket-mode.mjs +917 -0
  622. package/scripts/poller/slack-socket-mode.test.mjs +753 -0
  623. package/scripts/poller/trigger.mjs +75 -0
  624. package/scripts/poller/utils.mjs +371 -0
  625. package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
  626. package/scripts/poller/voice-poller.mjs +236 -0
  627. package/scripts/poller-launchd/install.sh +66 -0
  628. package/scripts/poller-launchd/poller.plist.template +40 -0
  629. package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
  630. package/scripts/post-interaction-indexer.py +1598 -0
  631. package/scripts/pre-draft-context.py +994 -0
  632. package/scripts/pre_draft_lookup.py +258 -0
  633. package/scripts/rag/build-index.mjs +47 -0
  634. package/scripts/rag/ingest.mjs +111 -0
  635. package/scripts/rag/search.mjs +119 -0
  636. package/scripts/rag-indexer.py +629 -0
  637. package/scripts/restore-from-backup.sh +248 -0
  638. package/scripts/restore-from-backup.test.mjs +178 -0
  639. package/scripts/resume-operations.sh +80 -0
  640. package/scripts/search-secondary-inbox.py +181 -0
  641. package/scripts/secondary-inbox-poller.py +437 -0
  642. package/scripts/self-optimization/compute-metrics.py +398 -0
  643. package/scripts/send-email-as-principal.py +369 -0
  644. package/scripts/send-email-threaded.py +392 -0
  645. package/scripts/send-email-with-attachment.py +377 -0
  646. package/scripts/send-email.sh +131 -0
  647. package/scripts/send-sms.sh +175 -0
  648. package/scripts/send-whatsapp.sh +292 -0
  649. package/scripts/session-start.sh +106 -0
  650. package/scripts/setup/boot-claude-session.sh +94 -0
  651. package/scripts/setup/configure-macos.sh +674 -0
  652. package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
  653. package/scripts/setup/configure-voice-tunnel.mjs +182 -0
  654. package/scripts/setup/generate-agent-env.mjs +92 -0
  655. package/scripts/setup/generate-agent-package-json.mjs +222 -0
  656. package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
  657. package/scripts/setup/generate-autonomy.mjs +60 -0
  658. package/scripts/setup/generate-backlog.mjs +101 -0
  659. package/scripts/setup/generate-cadences.mjs +92 -0
  660. package/scripts/setup/generate-capability.mjs +162 -0
  661. package/scripts/setup/generate-charter.mjs +76 -0
  662. package/scripts/setup/generate-comms.mjs +59 -0
  663. package/scripts/setup/generate-company.mjs +92 -0
  664. package/scripts/setup/init-agent.sh +547 -0
  665. package/scripts/setup/init-agent.test.mjs +151 -0
  666. package/scripts/setup/init-archetype.mjs +74 -0
  667. package/scripts/setup/init-backup.mjs +54 -0
  668. package/scripts/setup/init-cadence-bus.mjs +60 -0
  669. package/scripts/setup/init-channel-bus.mjs +46 -0
  670. package/scripts/setup/init-cost-tracking.mjs +45 -0
  671. package/scripts/setup/init-decision-capture.mjs +66 -0
  672. package/scripts/setup/init-known-agents.mjs +57 -0
  673. package/scripts/setup/init-learning.mjs +70 -0
  674. package/scripts/setup/init-memory-executive.mjs +45 -0
  675. package/scripts/setup/init-model-router.mjs +124 -0
  676. package/scripts/setup/init-rag.mjs +174 -0
  677. package/scripts/setup/init-session-router.mjs +38 -0
  678. package/scripts/setup/init-slack-socket-mode.mjs +260 -0
  679. package/scripts/setup/init-telegram.mjs +165 -0
  680. package/scripts/setup/init-voice-realtime.mjs +204 -0
  681. package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
  682. package/scripts/setup/install-dev-tools.sh +150 -0
  683. package/scripts/setup/lib/install-plist.mjs +95 -0
  684. package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
  685. package/scripts/setup/render-environment-yaml.mjs +133 -0
  686. package/scripts/slack-events-ctl.sh +177 -0
  687. package/scripts/slack-events-server.mjs +1045 -0
  688. package/scripts/slack-react.mjs +89 -0
  689. package/scripts/slack-responded.sh +232 -0
  690. package/scripts/slack-send.sh +287 -0
  691. package/scripts/slack-typing.mjs +196 -0
  692. package/scripts/slack-upload-v2.py +95 -0
  693. package/scripts/sms-handler.mjs +450 -0
  694. package/scripts/spawn-session.sh +120 -0
  695. package/scripts/sync-protocol.mjs +217 -0
  696. package/scripts/system-verify.sh +184 -0
  697. package/scripts/test-email-thread-dedup.py +239 -0
  698. package/scripts/test-information-barriers.py +484 -0
  699. package/scripts/test-llm-email-dedup.py +251 -0
  700. package/scripts/test-pre-draft-integration.py +203 -0
  701. package/scripts/test-rag-phase2.sh +442 -0
  702. package/scripts/test-rag-search.sh +251 -0
  703. package/scripts/test-voice-parser.mjs +316 -0
  704. package/scripts/user-context-search.py +659 -0
  705. package/scripts/validate_outbound.py +1504 -0
  706. package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
  707. package/scripts/watchdog/force-reboot.sh +157 -0
  708. package/scripts/watchdog/memory-watchdog.sh +473 -0
  709. package/scripts/whatsapp-handler.mjs +538 -0
  710. package/teams/desktop-operations.yaml +34 -0
  711. package/teams/executive-office.yaml +27 -0
  712. package/teams/legal-and-regulatory.yaml +24 -0
  713. package/teams/platform-and-engineering.yaml +23 -0
  714. package/teams/strategy-and-growth.yaml +29 -0
  715. package/workflows/continuous/backlog-executor.yaml +141 -0
  716. package/workflows/continuous/inbound-monitor.yaml +168 -0
  717. package/workflows/daily/applicant-triage.yaml +197 -0
  718. package/workflows/daily/comms-triage.yaml +80 -0
  719. package/workflows/daily/evening-wrap.yaml +105 -0
  720. package/workflows/daily/morning-brief.yaml +164 -0
  721. package/workflows/daily/slack-followup-sweep.yaml +87 -0
  722. package/workflows/event-driven/README.md +50 -0
  723. package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
  724. package/workflows/event-driven/pr-review.yaml +107 -0
  725. package/workflows/monthly/board-readiness.yaml +76 -0
  726. package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
  727. package/workflows/session-protocol.md +171 -0
  728. package/workflows/weekly/engineering-health.yaml +154 -0
  729. package/workflows/weekly/hiring-review.yaml +169 -0
  730. package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
  731. package/workflows/weekly/strategic-memo.yaml +79 -0
@@ -0,0 +1,453 @@
1
+ /**
2
+ * lib/learning/curator.mjs — deterministic skill lifecycle (WS3).
3
+ *
4
+ * Pure state-machine over the usage sidecar (state/learning/skills.json), run
5
+ * as a low-frequency `skill-curator` cadence. No LLM:
6
+ *
7
+ * active ──(30d no activity)──▶ stale
8
+ * stale ──(90d no activity)──▶ archived
9
+ *
10
+ * `last_activity` is bumped by the reflect pass whenever a skill id appears in a
11
+ * transcript (lib/learning/skill-writer.touchSkill). ARCHIVE means MOVE the
12
+ * `.md` into plugins/agent-skills/skills/_archive/ and DROP it from plugin.json
13
+ * — never delete, so a re-surfaced procedure can be restored. `pinned` skills
14
+ * are exempt from every transition.
15
+ *
16
+ * Fail-open: any FS error on an individual skill is recorded but never aborts
17
+ * the sweep; the function never throws.
18
+ *
19
+ * @module lib/learning/curator
20
+ */
21
+
22
+ "use strict";
23
+
24
+ import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
25
+ import { join, dirname } from "node:path";
26
+ import { readSidecar, writeSidecar, skillsDir, writeSkill, appendSkillAudit } from "./skill-writer.mjs";
27
+
28
+ const PLUGIN_JSON_REL = join("plugins", "agent-skills", "plugin.json");
29
+ const ARCHIVE_SUBDIR = "_archive";
30
+ const DAY_MS = 86400_000;
31
+
32
+ /** Consolidation defaults (mirror config/learning.yaml consolidation block). */
33
+ export const DEFAULT_MIN_SKILLS = 5;
34
+ export const DEFAULT_CONSOLIDATION_INTERVAL_DAYS = 14;
35
+ const CONSOLIDATE_TIMEOUT_MS = 120_000;
36
+ const SKILL_BODY_EXCERPT_CHARS = 1200;
37
+ const MAX_SKILLS_IN_PROMPT = 40;
38
+
39
+ /** Default thresholds (days). Overridable via runCurator opts / config. */
40
+ export const DEFAULT_STALE_DAYS = 30;
41
+ export const DEFAULT_ARCHIVE_DAYS = 90;
42
+
43
+ /** Age in ms of a sidecar record relative to `nowMs`. Falls back to created_at. */
44
+ function ageMs(rec, nowMs) {
45
+ const stamp = rec.last_activity || rec.created_at;
46
+ const t = Date.parse(stamp);
47
+ if (!Number.isFinite(t)) return 0; // unknown stamp → treat as fresh (no transition)
48
+ return nowMs - t;
49
+ }
50
+
51
+ /**
52
+ * Compute the next state for one record given its age. Pure — the transition
53
+ * core, unit-tested directly.
54
+ *
55
+ * pinned → unchanged (exempt)
56
+ * active, age ≥ archiveDays → archived (skip straight past stale)
57
+ * active, age ≥ staleDays → stale
58
+ * stale, age ≥ archiveDays → archived
59
+ * else → unchanged
60
+ *
61
+ * @param {object} rec - sidecar record { state, pinned, last_activity, created_at }
62
+ * @param {object} opts - { nowMs, staleDays, archiveDays }
63
+ * @returns {string} the next state
64
+ */
65
+ export function nextState(rec, opts = {}) {
66
+ const cur = rec.state || "active";
67
+ if (rec.pinned === true) return cur;
68
+ if (cur === "archived") return cur;
69
+ const nowMs = Number.isFinite(opts.nowMs) ? opts.nowMs : Date.now();
70
+ const staleDays = Number.isFinite(opts.staleDays) ? opts.staleDays : DEFAULT_STALE_DAYS;
71
+ const archiveDays = Number.isFinite(opts.archiveDays) ? opts.archiveDays : DEFAULT_ARCHIVE_DAYS;
72
+ const age = ageMs(rec, nowMs);
73
+ if (age >= archiveDays * DAY_MS) return "archived";
74
+ if (cur === "active" && age >= staleDays * DAY_MS) return "stale";
75
+ return cur;
76
+ }
77
+
78
+ /** Atomic file write (tmp + rename in the same dir). */
79
+ function writeAtomicFile(absPath, contents) {
80
+ mkdirSync(dirname(absPath), { recursive: true });
81
+ const tmp = `${absPath}.${process.pid}.${Date.now()}.tmp`;
82
+ writeFileSync(tmp, contents);
83
+ renameSync(tmp, absPath);
84
+ }
85
+
86
+ /** Drop a skill id (by manifest `name`) from plugin.json, atomically. Fail-open. */
87
+ function dropFromPlugin(agentRoot, id) {
88
+ try {
89
+ const p = join(agentRoot, PLUGIN_JSON_REL);
90
+ if (!existsSync(p)) return;
91
+ const manifest = JSON.parse(readFileSync(p, "utf8"));
92
+ if (!manifest || !Array.isArray(manifest.skills)) return;
93
+ const before = manifest.skills.length;
94
+ manifest.skills = manifest.skills.filter((s) => s && s.name !== id);
95
+ if (manifest.skills.length !== before) {
96
+ writeAtomicFile(p, JSON.stringify(manifest, null, 2) + "\n");
97
+ }
98
+ } catch { /* fail-open */ }
99
+ }
100
+
101
+ /**
102
+ * Move a skill .md into skills/_archive/. Returns true if the file was moved
103
+ * (or was already absent — archive is idempotent). Fail-open → false on error.
104
+ */
105
+ function archiveFile(agentRoot, id) {
106
+ try {
107
+ const dir = skillsDir(agentRoot);
108
+ const from = join(dir, `${id}.md`);
109
+ const archiveDir = join(dir, ARCHIVE_SUBDIR);
110
+ const to = join(archiveDir, `${id}.md`);
111
+ if (!existsSync(from)) return true; // nothing to move (already archived/removed)
112
+ mkdirSync(archiveDir, { recursive: true });
113
+ renameSync(from, to);
114
+ return true;
115
+ } catch {
116
+ return false;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Run one curator sweep over the sidecar. Applies active→stale→archived
122
+ * transitions, performs the archive move + plugin.json drop on newly-archived
123
+ * skills, and persists the updated sidecar. Deterministic; never throws.
124
+ *
125
+ * @param {object} o
126
+ * @param {string} o.agentRoot
127
+ * @param {number|Date} [o.now] injectable clock (tests)
128
+ * @param {number} [o.staleDays]
129
+ * @param {number} [o.archiveDays]
130
+ * @returns {{transitioned:Array<{id,from,to}>, archived:string[], errors:string[]}}
131
+ */
132
+ export function runCurator(o = {}) {
133
+ const agentRoot = o.agentRoot;
134
+ const out = { transitioned: [], archived: [], errors: [] };
135
+ if (!agentRoot) return out;
136
+
137
+ const nowMs = o.now instanceof Date ? o.now.getTime() : (Number.isFinite(o.now) ? o.now : Date.now());
138
+ const sidecar = readSidecar(agentRoot);
139
+ let dirty = false;
140
+
141
+ for (const id of Object.keys(sidecar)) {
142
+ const rec = sidecar[id];
143
+ if (!rec || typeof rec !== "object") continue;
144
+ const from = rec.state || "active";
145
+ const to = nextState(rec, { nowMs, staleDays: o.staleDays, archiveDays: o.archiveDays });
146
+ if (to === from) continue;
147
+
148
+ if (to === "archived") {
149
+ const moved = archiveFile(agentRoot, id);
150
+ if (!moved) { out.errors.push(id); continue; } // leave state as-is; retry next sweep
151
+ dropFromPlugin(agentRoot, id);
152
+ out.archived.push(id);
153
+ }
154
+ rec.state = to;
155
+ rec.archived_at = to === "archived" ? new Date(nowMs).toISOString() : rec.archived_at;
156
+ out.transitioned.push({ id, from, to });
157
+ dirty = true;
158
+ }
159
+
160
+ if (dirty) writeSidecar(agentRoot, sidecar);
161
+ return out;
162
+ }
163
+
164
+ /**
165
+ * Cheap guard for the guarded cadence: is there at least one non-pinned skill
166
+ * that would transition on a sweep right now? Lets the cadence handler skip the
167
+ * sweep entirely when nothing has crossed a boundary. Pure-ish (reads sidecar).
168
+ * @param {string} agentRoot
169
+ * @param {object} [opts] - { now, staleDays, archiveDays }
170
+ * @returns {boolean}
171
+ */
172
+ export function hasPendingTransitions(agentRoot, opts = {}) {
173
+ if (!agentRoot) return false;
174
+ const nowMs = opts.now instanceof Date ? opts.now.getTime() : (Number.isFinite(opts.now) ? opts.now : Date.now());
175
+ const sidecar = readSidecar(agentRoot);
176
+ for (const id of Object.keys(sidecar)) {
177
+ const rec = sidecar[id];
178
+ if (!rec || typeof rec !== "object") continue;
179
+ if (nextState(rec, { nowMs, staleDays: opts.staleDays, archiveDays: opts.archiveDays }) !== (rec.state || "active")) {
180
+ return true;
181
+ }
182
+ }
183
+ return false;
184
+ }
185
+
186
+ // ===========================================================================
187
+ // LLM umbrella consolidation (WS3 phase 2)
188
+ // ---------------------------------------------------------------------------
189
+ // The deterministic transitions above are housekeeping; consolidation is the
190
+ // one place the curator needs judgement. It MERGES narrowly-overlapping
191
+ // agent-authored skills into ONE class-level umbrella skill, so a swarm of
192
+ // near-duplicate procedures collapses into a reusable parent. The LLM is a
193
+ // bounded, injectable function (default: one `claude --print` sonnet call built
194
+ // like reflect.mjs); it runs as a DETACHED worker from the skill-curator guard,
195
+ // never as a nested sub-session. Fully fail-open + time-boxed: any throw → a
196
+ // no-op summary, never an exception.
197
+ // ===========================================================================
198
+
199
+ /** Read a skill's .md (name/description from frontmatter + a body excerpt). Fail-open. */
200
+ function readSkillSummary(agentRoot, id) {
201
+ try {
202
+ const p = join(skillsDir(agentRoot), `${id}.md`);
203
+ if (!existsSync(p)) return null;
204
+ const md = readFileSync(p, "utf8");
205
+ // Frontmatter is a leading --- … --- block (skill-writer renderSkillMd).
206
+ let name = id, description = "", body = md;
207
+ const fm = md.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
208
+ if (fm) {
209
+ body = fm[2] || "";
210
+ for (const line of fm[1].split("\n")) {
211
+ const m = line.match(/^(name|description):\s*(.*)$/);
212
+ if (m && m[1] === "name") name = m[2].trim() || id;
213
+ else if (m && m[1] === "description") description = m[2].trim();
214
+ }
215
+ }
216
+ const excerpt = String(body).trim().slice(0, SKILL_BODY_EXCERPT_CHARS);
217
+ return { id, name, description, excerpt };
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Select consolidation candidates: agent-authored, active, non-pinned skills
225
+ * that still have a .md on disk. Returns enriched summaries (capped). Pure-ish.
226
+ * @param {string} agentRoot
227
+ * @returns {Array<{id,name,description,excerpt}>}
228
+ */
229
+ export function selectConsolidationCandidates(agentRoot) {
230
+ const sidecar = readSidecar(agentRoot);
231
+ const out = [];
232
+ for (const id of Object.keys(sidecar)) {
233
+ const rec = sidecar[id];
234
+ if (!rec || typeof rec !== "object") continue;
235
+ if (rec.created_by !== "agent") continue;
236
+ if ((rec.state || "active") !== "active") continue;
237
+ if (rec.pinned === true) continue;
238
+ const summary = readSkillSummary(agentRoot, id);
239
+ if (summary) out.push(summary);
240
+ if (out.length >= MAX_SKILLS_IN_PROMPT) break;
241
+ }
242
+ return out;
243
+ }
244
+
245
+ /**
246
+ * Build the consolidation prompt (ports the intent of Hermes's CURATOR_REVIEW
247
+ * prompt). Asks the model to cluster the candidates and propose ONE umbrella per
248
+ * cluster of ≥2 genuinely-overlapping siblings; strict-JSON output, conservative.
249
+ * @param {Array<{id,name,description,excerpt}>} candidates
250
+ * @returns {string}
251
+ */
252
+ export function buildConsolidationPrompt(candidates) {
253
+ const lines = [
254
+ "You are the skill CURATOR for an autonomous AI agent. Over time the agent",
255
+ "has authored many narrow skills. Your job is to consolidate genuinely-",
256
+ "overlapping siblings into a single CLASS-LEVEL umbrella skill that subsumes",
257
+ "them — so the agent reaches for one well-scoped parent instead of a dozen",
258
+ "near-duplicates.",
259
+ "",
260
+ "Cluster the skills below. For each cluster of TWO OR MORE narrowly-",
261
+ "overlapping skills, propose ONE umbrella skill that covers all of them as a",
262
+ "general, re-runnable method. Reuse the concrete steps from the members.",
263
+ "",
264
+ "Be CONSERVATIVE. Only merge genuinely-overlapping siblings:",
265
+ " - DO NOT merge skills from unrelated domains (e.g. a billing-reconciliation",
266
+ " skill and a calendar-scheduling skill are NOT siblings — leave them).",
267
+ " - DO NOT merge a single skill into an 'umbrella' (a cluster needs ≥2).",
268
+ " - DO NOT invent capabilities the members don't have; the umbrella must be a",
269
+ " faithful generalisation of its absorbed members, nothing more.",
270
+ " - When in doubt, leave skills separate. A missed merge is cheap; a wrong",
271
+ " merge buries two distinct procedures under one vague parent.",
272
+ "",
273
+ "Each umbrella's `absorbs` MUST list the exact skill ids (from the list) it",
274
+ "replaces. Those skills will be archived and the umbrella created in their",
275
+ "place. Never list an id that isn't in the candidate list below.",
276
+ "",
277
+ "Respond with STRICT JSON and nothing else (no prose, no code fence):",
278
+ ' {"umbrellas": []} — if nothing should be merged, OR',
279
+ ' {"umbrellas": [{',
280
+ ' "name": "<short class-level name>",',
281
+ ' "description": "<one line: when to use this umbrella>",',
282
+ ' "body": "<markdown: trigger, ordered steps covering the members, success criteria>",',
283
+ ' "absorbs": ["<skill-id>", "<skill-id>", ...] // ≥2 ids from the list',
284
+ " }]}",
285
+ "",
286
+ "Default to {\"umbrellas\": []} unless a cluster is clearly overlapping.",
287
+ "",
288
+ "=== CANDIDATE SKILLS (agent-authored, active) ===",
289
+ ];
290
+ for (const c of candidates) {
291
+ lines.push(
292
+ `- id: ${c.id}`,
293
+ ` name: ${c.name}`,
294
+ ` description: ${c.description}`,
295
+ ` body: ${JSON.stringify(c.excerpt)}`,
296
+ );
297
+ }
298
+ lines.push("=== END ===");
299
+ return lines.join("\n");
300
+ }
301
+
302
+ /**
303
+ * Parse + validate the consolidation envelope against the known candidate ids.
304
+ * Filters absorbs to real candidate ids, rejects clusters of <2 valid ids, and
305
+ * (defensively) prevents the same id being absorbed by two umbrellas. Pure.
306
+ * @param {string} text
307
+ * @param {Set<string>} validIds
308
+ * @returns {Array<{name,description,body,absorbs:string[]}>}
309
+ */
310
+ export function parseConsolidationEnvelope(text, validIds) {
311
+ if (!text || typeof text !== "string") return [];
312
+ const start = text.indexOf("{");
313
+ const end = text.lastIndexOf("}");
314
+ if (start === -1 || end === -1 || end <= start) return [];
315
+ let obj;
316
+ try { obj = JSON.parse(text.slice(start, end + 1)); }
317
+ catch { return []; }
318
+ if (!obj || typeof obj !== "object" || !Array.isArray(obj.umbrellas)) return [];
319
+ const claimed = new Set();
320
+ const out = [];
321
+ for (const u of obj.umbrellas) {
322
+ if (!u || typeof u !== "object") continue;
323
+ const name = String(u.name || "").trim();
324
+ const description = String(u.description || "").trim();
325
+ const body = String(u.body || "").trim();
326
+ if (!name || !body) continue;
327
+ const absorbs = Array.isArray(u.absorbs)
328
+ ? Array.from(new Set(u.absorbs.map((s) => String(s || "").trim())))
329
+ .filter((id) => id && validIds.has(id) && !claimed.has(id))
330
+ : [];
331
+ if (absorbs.length < 2) continue; // never merge a single skill into an umbrella
332
+ for (const id of absorbs) claimed.add(id);
333
+ out.push({ name, description, body, absorbs });
334
+ }
335
+ return out;
336
+ }
337
+
338
+ /** Default consolidation spawn: one `claude --print --model <sonnet>`. Fail-open → "". */
339
+ export async function defaultConsolidateSpawn(prompt, opts = {}) {
340
+ try {
341
+ const { spawnSync } = await import("node:child_process");
342
+ const { resolveClaudeBin, daemonClaudeArgs, augmentedPath } = await import("../claude-bin.mjs");
343
+ const model = opts.model || "sonnet";
344
+ const args = ["--print", "--model", model, ...daemonClaudeArgs()];
345
+ const res = spawnSync(resolveClaudeBin(), args, {
346
+ input: prompt,
347
+ encoding: "utf8",
348
+ timeout: opts.timeoutMs || CONSOLIDATE_TIMEOUT_MS,
349
+ env: { ...process.env, PATH: augmentedPath() },
350
+ maxBuffer: 8 * 1024 * 1024,
351
+ });
352
+ if (res.status === 0 && res.stdout) return res.stdout;
353
+ return "";
354
+ } catch {
355
+ return "";
356
+ }
357
+ }
358
+
359
+ /**
360
+ * Consolidate narrowly-overlapping agent-authored skills into class-level
361
+ * umbrellas. Reads the sidecar → selects candidates → (if ≥ minSkills) builds a
362
+ * conservative clustering prompt → calls the injectable `spawn` → for each valid
363
+ * umbrella: writes it via skill-writer (secret-scan + plugin.json + audit), then
364
+ * archives every absorbed member (move to _archive/ + drop from plugin.json +
365
+ * sidecar state:"archived"/absorbed_into) and appends a consolidate audit row.
366
+ *
367
+ * Time-boxed, FULLY fail-open: any throw → { umbrellas:[], error }; never throws.
368
+ *
369
+ * @param {object} o
370
+ * @param {string} o.agentRoot
371
+ * @param {Function} [o.spawn] injectable reviewer (tests; no network). default = claude --print
372
+ * @param {number|Date} [o.now]
373
+ * @param {number} [o.minSkills] candidate floor below which we don't spawn (default 5)
374
+ * @param {string} [o.model] reviewer model (default "sonnet")
375
+ * @returns {Promise<{umbrellas:Array<{id,absorbed:string[]}>, reason?:string, error?:string}>}
376
+ */
377
+ export async function consolidateUmbrellas(o = {}) {
378
+ try {
379
+ const agentRoot = o.agentRoot;
380
+ if (!agentRoot) return { umbrellas: [], reason: "no-agent-root" };
381
+ const minSkills = Number.isFinite(o.minSkills) ? o.minSkills : DEFAULT_MIN_SKILLS;
382
+ const nowMs = o.now instanceof Date ? o.now.getTime() : (Number.isFinite(o.now) ? o.now : Date.now());
383
+ const nowIso = new Date(nowMs).toISOString();
384
+
385
+ const candidates = selectConsolidationCandidates(agentRoot);
386
+ if (candidates.length < minSkills) {
387
+ return { umbrellas: [], reason: "below-threshold" };
388
+ }
389
+
390
+ const validIds = new Set(candidates.map((c) => c.id));
391
+ const prompt = buildConsolidationPrompt(candidates);
392
+ const spawn = o.spawn || defaultConsolidateSpawn;
393
+
394
+ let raw = "";
395
+ try {
396
+ raw = await spawn(prompt, { model: o.model || "sonnet", timeoutMs: CONSOLIDATE_TIMEOUT_MS, agentRoot });
397
+ } catch (e) {
398
+ // A throwing spawn is fail-open at the function boundary.
399
+ return { umbrellas: [], error: e && e.message ? e.message : String(e) };
400
+ }
401
+
402
+ const proposals = parseConsolidationEnvelope(raw, validIds);
403
+ if (!proposals.length) return { umbrellas: [], reason: "no-merge" };
404
+
405
+ const applied = [];
406
+ for (const u of proposals) {
407
+ // 1. Create the umbrella (skill-writer secret-scans + writes + plugin.json + audit).
408
+ // trusted:true — this is system-internal consolidation, not untrusted
409
+ // agent-authored output, so the umbrella lands live (not quarantined).
410
+ const wr = writeSkill({
411
+ agentRoot, action: "create", name: u.name, description: u.description, body: u.body,
412
+ sourceSession: "skill-curator", nowIso, trusted: true,
413
+ });
414
+ if (!wr.ok) {
415
+ appendSkillAudit(agentRoot, { action: "consolidate_abort", name: u.name, reason: wr.reason, absorbs: u.absorbs });
416
+ continue; // skip this cluster; don't archive members if the umbrella didn't land
417
+ }
418
+
419
+ // 2. Archive each absorbed member: move .md → _archive/, drop from plugin.json,
420
+ // mark the sidecar record archived + absorbed_into the umbrella.
421
+ const sidecar = readSidecar(agentRoot);
422
+ const absorbed = [];
423
+ for (const id of u.absorbs) {
424
+ if (id === wr.id) continue; // never absorb the umbrella into itself
425
+ const moved = archiveFile(agentRoot, id);
426
+ if (!moved) continue; // FS error on this member → leave it; never abort the batch
427
+ dropFromPlugin(agentRoot, id);
428
+ if (sidecar[id] && typeof sidecar[id] === "object") {
429
+ sidecar[id].state = "archived";
430
+ sidecar[id].archived_at = nowIso;
431
+ sidecar[id].absorbed_into = wr.id;
432
+ }
433
+ absorbed.push(id);
434
+ }
435
+ writeSidecar(agentRoot, sidecar);
436
+
437
+ // 3. Audit the consolidation.
438
+ appendSkillAudit(agentRoot, { action: "consolidate", umbrella: wr.id, absorbed });
439
+ applied.push({ id: wr.id, absorbed });
440
+ }
441
+
442
+ return { umbrellas: applied };
443
+ } catch (e) {
444
+ // Absolute fail-open: any unexpected throw → no umbrellas, never rethrow.
445
+ return { umbrellas: [], error: e && e.message ? e.message : String(e) };
446
+ }
447
+ }
448
+
449
+ export default {
450
+ runCurator, nextState, hasPendingTransitions, consolidateUmbrellas,
451
+ selectConsolidationCandidates, buildConsolidationPrompt, parseConsolidationEnvelope,
452
+ DEFAULT_STALE_DAYS, DEFAULT_ARCHIVE_DAYS, DEFAULT_MIN_SKILLS, DEFAULT_CONSOLIDATION_INTERVAL_DAYS,
453
+ };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * curator.test.mjs — deterministic skill lifecycle tests.
3
+ * Run: node --test lib/learning/curator.test.mjs
4
+ *
5
+ * Covers: 30d active→stale, 90d →archived, archive MOVES (not deletes) +
6
+ * drops from plugin.json, pinned exempt, hasPendingTransitions guard.
7
+ */
8
+ "use strict";
9
+
10
+ import { test } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { mkdtempSync, existsSync, readFileSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+
16
+ import { runCurator, nextState, hasPendingTransitions } from "./curator.mjs";
17
+ import { writeSkill, readSidecar, writeSidecar } from "./skill-writer.mjs";
18
+
19
+ function tmpRoot() {
20
+ return mkdtempSync(join(tmpdir(), "maestro-curator-"));
21
+ }
22
+ const skillPath = (root, id) => join(root, "plugins", "agent-skills", "skills", `${id}.md`);
23
+ const archivePath = (root, id) => join(root, "plugins", "agent-skills", "skills", "_archive", `${id}.md`);
24
+ const pluginJson = (root) => JSON.parse(readFileSync(join(root, "plugins", "agent-skills", "plugin.json"), "utf8"));
25
+
26
+ const NOW = Date.parse("2026-06-09T00:00:00Z");
27
+ const daysAgo = (n) => new Date(NOW - n * 86400_000).toISOString();
28
+
29
+ test("nextState: pure transitions by age", () => {
30
+ assert.equal(nextState({ state: "active", last_activity: daysAgo(5) }, { nowMs: NOW }), "active");
31
+ assert.equal(nextState({ state: "active", last_activity: daysAgo(31) }, { nowMs: NOW }), "stale");
32
+ assert.equal(nextState({ state: "stale", last_activity: daysAgo(95) }, { nowMs: NOW }), "archived");
33
+ // active that's been silent >90d jumps straight to archived.
34
+ assert.equal(nextState({ state: "active", last_activity: daysAgo(120) }, { nowMs: NOW }), "archived");
35
+ // pinned is exempt regardless of age.
36
+ assert.equal(nextState({ state: "active", pinned: true, last_activity: daysAgo(365) }, { nowMs: NOW }), "active");
37
+ // archived is terminal.
38
+ assert.equal(nextState({ state: "archived", last_activity: daysAgo(365) }, { nowMs: NOW }), "archived");
39
+ });
40
+
41
+ test("runCurator: active→stale at 30d, no file move", () => {
42
+ const root = tmpRoot();
43
+ const a = writeSkill({ agentRoot: root, name: "Aging Skill", description: "d", body: "x", nowIso: daysAgo(40), trusted: true });
44
+ // Backdate last_activity so it's stale-eligible.
45
+ const sc = readSidecar(root);
46
+ sc[a.id].last_activity = daysAgo(40);
47
+ writeSidecar(root, sc);
48
+
49
+ const res = runCurator({ agentRoot: root, now: NOW });
50
+ assert.deepEqual(res.transitioned, [{ id: a.id, from: "active", to: "stale" }]);
51
+ assert.deepEqual(res.archived, []);
52
+ assert.equal(readSidecar(root)[a.id].state, "stale");
53
+ assert.ok(existsSync(skillPath(root, a.id)), "stale skill still in place");
54
+ });
55
+
56
+ test("runCurator: stale→archived at 90d MOVES the file + drops from plugin.json (never deletes)", () => {
57
+ const root = tmpRoot();
58
+ const a = writeSkill({ agentRoot: root, name: "Old Skill", description: "d", body: "ARCHIVE ME", nowIso: daysAgo(120), trusted: true });
59
+ const sc = readSidecar(root);
60
+ sc[a.id].state = "stale";
61
+ sc[a.id].last_activity = daysAgo(120);
62
+ writeSidecar(root, sc);
63
+ assert.equal(pluginJson(root).skills.length, 1);
64
+
65
+ const res = runCurator({ agentRoot: root, now: NOW });
66
+ assert.deepEqual(res.archived, [a.id]);
67
+ assert.equal(readSidecar(root)[a.id].state, "archived");
68
+
69
+ // File MOVED — gone from skills/, present in skills/_archive/ with body intact.
70
+ assert.ok(!existsSync(skillPath(root, a.id)), "removed from active skills dir");
71
+ assert.ok(existsSync(archivePath(root, a.id)), "moved into _archive (not deleted)");
72
+ assert.match(readFileSync(archivePath(root, a.id), "utf8"), /ARCHIVE ME/);
73
+
74
+ // Dropped from plugin.json manifest.
75
+ assert.equal(pluginJson(root).skills.length, 0, "dropped from plugin.json");
76
+ });
77
+
78
+ test("runCurator: pinned skill is exempt from all transitions", () => {
79
+ const root = tmpRoot();
80
+ const a = writeSkill({ agentRoot: root, name: "Pinned Skill", description: "d", body: "keep", nowIso: daysAgo(400), trusted: true });
81
+ const sc = readSidecar(root);
82
+ sc[a.id].pinned = true;
83
+ sc[a.id].last_activity = daysAgo(400);
84
+ writeSidecar(root, sc);
85
+
86
+ const res = runCurator({ agentRoot: root, now: NOW });
87
+ assert.deepEqual(res.transitioned, []);
88
+ assert.equal(readSidecar(root)[a.id].state, "active");
89
+ assert.ok(existsSync(skillPath(root, a.id)), "pinned skill untouched");
90
+ });
91
+
92
+ test("hasPendingTransitions: true only when something would move", () => {
93
+ const root = tmpRoot();
94
+ const a = writeSkill({ agentRoot: root, name: "Fresh", description: "d", body: "x", nowIso: daysAgo(2), trusted: true });
95
+ assert.equal(hasPendingTransitions(root, { now: NOW }), false, "fresh → nothing pending");
96
+ const sc = readSidecar(root);
97
+ sc[a.id].last_activity = daysAgo(45);
98
+ writeSidecar(root, sc);
99
+ assert.equal(hasPendingTransitions(root, { now: NOW }), true, "45d-old active → stale pending");
100
+ });
101
+
102
+ test("runCurator: no agentRoot / empty sidecar is a clean no-op", () => {
103
+ assert.deepEqual(runCurator({}), { transitioned: [], archived: [], errors: [] });
104
+ const root = tmpRoot();
105
+ assert.deepEqual(runCurator({ agentRoot: root, now: NOW }), { transitioned: [], archived: [], errors: [] });
106
+ });
@@ -0,0 +1,40 @@
1
+ /**
2
+ * lib/learning/log.mjs — append-only decision log for the self-learning loop.
3
+ *
4
+ * Every reflect decision (skipped / no-skill / skill-written / write-failed)
5
+ * lands in logs/learning/<YYYY-MM-DD>.jsonl. This is the operator-facing trail
6
+ * that complements logs/audit/skills.jsonl (which is the security trail of every
7
+ * create/patch/abort). Fail-open: never throws.
8
+ *
9
+ * @module lib/learning/log
10
+ */
11
+
12
+ "use strict";
13
+
14
+ import { appendFileSync, mkdirSync } from "node:fs";
15
+ import { join } from "node:path";
16
+
17
+ const LOG_DIR_REL = join("logs", "learning");
18
+
19
+ /** UTC date stamp for the log filename. */
20
+ function dayStamp(nowMs) {
21
+ return new Date(Number.isFinite(nowMs) ? nowMs : Date.now()).toISOString().slice(0, 10);
22
+ }
23
+
24
+ /**
25
+ * Append one structured decision row to today's learning log. Fail-open.
26
+ * @param {string} agentRoot
27
+ * @param {object} row
28
+ * @param {object} [opts] - { nowMs }
29
+ */
30
+ export function appendLearningLog(agentRoot, row, opts = {}) {
31
+ if (!agentRoot) return;
32
+ try {
33
+ const dir = join(agentRoot, LOG_DIR_REL);
34
+ mkdirSync(dir, { recursive: true });
35
+ const file = join(dir, `${dayStamp(opts.nowMs)}.jsonl`);
36
+ appendFileSync(file, JSON.stringify({ ts: new Date(opts.nowMs || Date.now()).toISOString(), ...row }) + "\n");
37
+ } catch { /* fail-open */ }
38
+ }
39
+
40
+ export default { appendLearningLog };