@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,81 @@
1
+ /**
2
+ * sot.test.mjs — single-source-of-truth helper tests (lib/setup/sot.mjs).
3
+ * Covers wrapper generation idempotency + agent.json merge keeping unrelated keys.
4
+ * Run: `node --test lib/setup/sot.test.mjs`
5
+ * @module lib/setup/sot.test
6
+ */
7
+
8
+ "use strict";
9
+
10
+ import { test } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { mkdtempSync, writeFileSync, mkdirSync, readFileSync, existsSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join } from "node:path";
15
+ import {
16
+ readAgentJson, writeAgentJson, mergeAgentJson, deepMergeByKey,
17
+ ensureSotWrapper, isWrapper, agentTsPath, WRAPPER_TEMPLATE,
18
+ } from "./sot.mjs";
19
+
20
+ function tmpRoot() {
21
+ const r = mkdtempSync(join(tmpdir(), "maestro-sot-"));
22
+ mkdirSync(join(r, "config"), { recursive: true });
23
+ return r;
24
+ }
25
+
26
+ test("readAgentJson returns null when missing, object when present", () => {
27
+ const root = tmpRoot();
28
+ assert.equal(readAgentJson(root), null);
29
+ writeAgentJson(root, { firstName: "Jo" });
30
+ assert.deepEqual(readAgentJson(root), { firstName: "Jo" });
31
+ });
32
+
33
+ test("deepMergeByKey preserves unrelated keys and deep-merges objects", () => {
34
+ const base = { a: 1, nested: { x: 1, y: 2 }, arr: [1, 2] };
35
+ const patch = { nested: { y: 9, z: 3 }, arr: [3] };
36
+ const out = deepMergeByKey(base, patch);
37
+ assert.deepEqual(out, { a: 1, nested: { x: 1, y: 9, z: 3 }, arr: [3] });
38
+ // inputs not mutated
39
+ assert.deepEqual(base.nested, { x: 1, y: 2 });
40
+ });
41
+
42
+ test("mergeAgentJson keeps unrelated fields (split-brain guard)", () => {
43
+ const root = tmpRoot();
44
+ writeAgentJson(root, {
45
+ firstName: "OLD", title: "Role",
46
+ principal: { fullName: "P One", email: "p@x.com" },
47
+ responsibilities: ["a"],
48
+ });
49
+ mergeAgentJson(root, { firstName: "NEW", principal: { email: "p2@x.com" } });
50
+ const back = readAgentJson(root);
51
+ assert.equal(back.firstName, "NEW");
52
+ assert.equal(back.title, "Role", "unrelated key preserved");
53
+ assert.equal(back.principal.fullName, "P One", "nested unrelated key preserved");
54
+ assert.equal(back.principal.email, "p2@x.com", "nested key updated");
55
+ assert.deepEqual(back.responsibilities, ["a"], "array preserved");
56
+ });
57
+
58
+ test("isWrapper distinguishes wrapper from standalone literal", () => {
59
+ assert.equal(isWrapper(WRAPPER_TEMPLATE), true);
60
+ const standalone = `export const agent = {\n firstName: 'Ravi',\n} as const;\n`;
61
+ assert.equal(isWrapper(standalone), false);
62
+ assert.equal(isWrapper("nonsense"), false);
63
+ });
64
+
65
+ test("ensureSotWrapper writes wrapper when absent and is idempotent", () => {
66
+ const root = tmpRoot();
67
+ const first = ensureSotWrapper(root);
68
+ assert.equal(first.wrote, true);
69
+ assert.ok(existsSync(agentTsPath(root)));
70
+ assert.ok(isWrapper(readFileSync(agentTsPath(root), "utf-8")));
71
+ const second = ensureSotWrapper(root);
72
+ assert.equal(second.wrote, false, "second call is a no-op");
73
+ });
74
+
75
+ test("ensureSotWrapper replaces a standalone literal .ts", () => {
76
+ const root = tmpRoot();
77
+ writeFileSync(agentTsPath(root), `export const agent = { firstName: 'Ravi' } as const;\n`);
78
+ const r = ensureSotWrapper(root);
79
+ assert.equal(r.wrote, true);
80
+ assert.ok(isWrapper(readFileSync(agentTsPath(root), "utf-8")));
81
+ });
@@ -0,0 +1,151 @@
1
+ /**
2
+ * lib/setup/state.mjs — durable checkpoint store for the `maestro setup` wizard.
3
+ *
4
+ * Persists to `state/setup/progress.json`. The checkpoint is a *hint*: on resume
5
+ * the runner re-`detect`s every section (the on-disk SoT is authoritative), but
6
+ * the checkpoint records what was applied, when, the verify result, and a hash
7
+ * of the answers that produced it (so a changed answer invalidates the skip).
8
+ *
9
+ * Schema (per §1 wizard / §4 design):
10
+ * {
11
+ * version: 1,
12
+ * updatedAt: ISO,
13
+ * answers: { ...durable answers across sections },
14
+ * sections: {
15
+ * <id>: { status, applied_at, verify, answersHash }
16
+ * },
17
+ * enrich: { <fieldUnit>: { status, at, reason? } } // enrichment ledger
18
+ * }
19
+ *
20
+ * Atomic writes (tmp + rename). Fail-soft on read (bad/missing file → fresh
21
+ * empty progress) so a corrupt checkpoint never blocks setup.
22
+ *
23
+ * @module lib/setup/state
24
+ */
25
+
26
+ "use strict";
27
+
28
+ import { existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
29
+ import { join, dirname } from "node:path";
30
+ import { createHash } from "node:crypto";
31
+
32
+ export const PROGRESS_REL = "state/setup/progress.json";
33
+ export const SCHEMA_VERSION = 1;
34
+
35
+ /** Absolute path to the progress file for an agent root. */
36
+ export function progressPath(agentRoot) {
37
+ return join(agentRoot, PROGRESS_REL);
38
+ }
39
+
40
+ /** A fresh, empty progress object. */
41
+ export function emptyProgress() {
42
+ return {
43
+ version: SCHEMA_VERSION,
44
+ updatedAt: null,
45
+ answers: {},
46
+ sections: {},
47
+ enrich: {},
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Stable hash of an answers object (order-independent). Used to detect when a
53
+ * section's inputs changed so a previously-complete section is re-run.
54
+ * @param {object} answers
55
+ * @returns {string} 16-char hex digest
56
+ */
57
+ export function hashAnswers(answers) {
58
+ const canonical = canonicalize(answers == null ? {} : answers);
59
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
60
+ }
61
+
62
+ function canonicalize(value) {
63
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
64
+ if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
65
+ const keys = Object.keys(value).sort();
66
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(value[k])}`).join(",")}}`;
67
+ }
68
+
69
+ /**
70
+ * Load the checkpoint for an agent root, returning a fresh empty progress when
71
+ * the file is missing or unreadable/corrupt (never throws).
72
+ * @param {string} agentRoot
73
+ * @returns {object}
74
+ */
75
+ export function loadProgress(agentRoot) {
76
+ const p = progressPath(agentRoot);
77
+ if (!existsSync(p)) return emptyProgress();
78
+ try {
79
+ const raw = JSON.parse(readFileSync(p, "utf-8"));
80
+ if (!raw || typeof raw !== "object") return emptyProgress();
81
+ return {
82
+ version: raw.version || SCHEMA_VERSION,
83
+ updatedAt: raw.updatedAt || null,
84
+ answers: raw.answers && typeof raw.answers === "object" ? raw.answers : {},
85
+ sections: raw.sections && typeof raw.sections === "object" ? raw.sections : {},
86
+ enrich: raw.enrich && typeof raw.enrich === "object" ? raw.enrich : {},
87
+ };
88
+ } catch {
89
+ return emptyProgress();
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Atomically persist a progress object. Stamps `updatedAt`.
95
+ * @param {string} agentRoot
96
+ * @param {object} progress
97
+ */
98
+ export function saveProgress(agentRoot, progress) {
99
+ const p = progressPath(agentRoot);
100
+ mkdirSync(dirname(p), { recursive: true });
101
+ const body = { ...emptyProgress(), ...progress, version: SCHEMA_VERSION, updatedAt: new Date().toISOString() };
102
+ const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
103
+ writeFileSync(tmp, JSON.stringify(body, null, 2) + "\n", "utf-8");
104
+ renameSync(tmp, p);
105
+ return body;
106
+ }
107
+
108
+ /**
109
+ * Record a section's status into the checkpoint and persist atomically.
110
+ * Returns the updated progress object.
111
+ * @param {string} agentRoot
112
+ * @param {string} sectionId
113
+ * @param {{status:string, verify?:object, answersHash?:string, answers?:object}} patch
114
+ */
115
+ export function markSection(agentRoot, sectionId, patch = {}) {
116
+ const progress = loadProgress(agentRoot);
117
+ const prev = progress.sections[sectionId] || {};
118
+ progress.sections[sectionId] = {
119
+ status: patch.status || prev.status || "unknown",
120
+ applied_at: patch.status ? new Date().toISOString() : prev.applied_at || null,
121
+ verify: patch.verify !== undefined ? patch.verify : prev.verify || null,
122
+ answersHash: patch.answersHash !== undefined ? patch.answersHash : prev.answersHash || null,
123
+ };
124
+ if (patch.answers && typeof patch.answers === "object") {
125
+ progress.answers = { ...progress.answers, ...patch.answers };
126
+ }
127
+ return saveProgress(agentRoot, progress);
128
+ }
129
+
130
+ /**
131
+ * Record an enrichment unit outcome in the ledger.
132
+ * @param {string} agentRoot
133
+ * @param {string} unit enrichable field id (e.g. "agent.persona")
134
+ * @param {{status:'enriched'|'pending'|'skipped', reason?:string}} patch
135
+ */
136
+ export function markEnrich(agentRoot, unit, patch = {}) {
137
+ const progress = loadProgress(agentRoot);
138
+ progress.enrich[unit] = {
139
+ status: patch.status || "pending",
140
+ at: new Date().toISOString(),
141
+ ...(patch.reason ? { reason: patch.reason } : {}),
142
+ };
143
+ return saveProgress(agentRoot, progress);
144
+ }
145
+
146
+ /** Merge durable answers into the checkpoint (no section change). */
147
+ export function mergeAnswers(agentRoot, answers) {
148
+ const progress = loadProgress(agentRoot);
149
+ progress.answers = { ...progress.answers, ...(answers || {}) };
150
+ return saveProgress(agentRoot, progress);
151
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * state.test.mjs — checkpoint round-trip / resume tests for lib/setup/state.mjs.
3
+ * Run: `node --test lib/setup/state.test.mjs`
4
+ * @module lib/setup/state.test
5
+ */
6
+
7
+ "use strict";
8
+
9
+ import { test } from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { mkdtempSync, writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import {
15
+ loadProgress, saveProgress, markSection, markEnrich, mergeAnswers,
16
+ hashAnswers, progressPath, emptyProgress,
17
+ } from "./state.mjs";
18
+
19
+ function tmpRoot() {
20
+ return mkdtempSync(join(tmpdir(), "maestro-state-"));
21
+ }
22
+
23
+ test("loadProgress returns empty progress for a fresh root", () => {
24
+ const root = tmpRoot();
25
+ const p = loadProgress(root);
26
+ assert.equal(p.version, 1);
27
+ assert.deepEqual(p.sections, {});
28
+ assert.deepEqual(p.answers, {});
29
+ });
30
+
31
+ test("saveProgress + loadProgress round-trip with atomic write", () => {
32
+ const root = tmpRoot();
33
+ const p = emptyProgress();
34
+ p.answers.firstName = "Jacob";
35
+ saveProgress(root, p);
36
+ assert.ok(existsSync(progressPath(root)), "progress.json must exist");
37
+ const back = loadProgress(root);
38
+ assert.equal(back.answers.firstName, "Jacob");
39
+ assert.ok(back.updatedAt, "updatedAt stamped");
40
+ });
41
+
42
+ test("markSection records status + applied_at and persists", () => {
43
+ const root = tmpRoot();
44
+ markSection(root, "identity", { status: "complete", answersHash: "abc123", verify: { ok: true } });
45
+ const p = loadProgress(root);
46
+ assert.equal(p.sections.identity.status, "complete");
47
+ assert.equal(p.sections.identity.answersHash, "abc123");
48
+ assert.deepEqual(p.sections.identity.verify, { ok: true });
49
+ assert.ok(p.sections.identity.applied_at);
50
+ });
51
+
52
+ test("markSection merges durable answers", () => {
53
+ const root = tmpRoot();
54
+ markSection(root, "identity", { status: "complete", answers: { firstName: "Mara" } });
55
+ markSection(root, "company", { status: "complete", answers: { company: "Acme" } });
56
+ const p = loadProgress(root);
57
+ assert.equal(p.answers.firstName, "Mara");
58
+ assert.equal(p.answers.company, "Acme");
59
+ });
60
+
61
+ test("hashAnswers is order-independent and changes with content", () => {
62
+ const a = hashAnswers({ x: 1, y: 2 });
63
+ const b = hashAnswers({ y: 2, x: 1 });
64
+ const c = hashAnswers({ x: 1, y: 3 });
65
+ assert.equal(a, b, "key order must not matter");
66
+ assert.notEqual(a, c, "content change must change hash");
67
+ });
68
+
69
+ test("markEnrich records ledger entries", () => {
70
+ const root = tmpRoot();
71
+ markEnrich(root, "agent.persona", { status: "pending", reason: "llm failed" });
72
+ const p = loadProgress(root);
73
+ assert.equal(p.enrich["agent.persona"].status, "pending");
74
+ assert.equal(p.enrich["agent.persona"].reason, "llm failed");
75
+ });
76
+
77
+ test("corrupt progress file falls back to empty (never throws)", () => {
78
+ const root = tmpRoot();
79
+ mkdirSync(join(root, "state/setup"), { recursive: true });
80
+ writeFileSync(progressPath(root), "{ not json ");
81
+ const p = loadProgress(root);
82
+ assert.deepEqual(p.sections, {});
83
+ });
84
+
85
+ test("mergeAnswers updates without disturbing sections", () => {
86
+ const root = tmpRoot();
87
+ markSection(root, "identity", { status: "complete" });
88
+ mergeAnswers(root, { tone: "warm" });
89
+ const p = loadProgress(root);
90
+ assert.equal(p.answers.tone, "warm");
91
+ assert.equal(p.sections.identity.status, "complete");
92
+ });
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Maestro — Process Singleton Guard
3
+ *
4
+ * Prevents duplicate instances of long-running services (daemon, events
5
+ * server, huddle server, etc.). Uses PID files with stale detection.
6
+ *
7
+ * Usage:
8
+ * import { acquireLock, releaseLock } from "@cohortapp/agent-sdk/lib/singleton.js";
9
+ *
10
+ * // At startup:
11
+ * if (!acquireLock("slack-events")) {
12
+ * console.log("Already running — exiting");
13
+ * process.exit(0);
14
+ * }
15
+ *
16
+ * // On shutdown:
17
+ * process.on("SIGTERM", () => { releaseLock("slack-events"); process.exit(0); });
18
+ *
19
+ * @module singleton
20
+ */
21
+
22
+ import { readFileSync, unlinkSync, existsSync, mkdirSync, openSync, writeSync, closeSync } from "node:fs";
23
+ import { join } from "node:path";
24
+ import { execFileSync } from "node:child_process";
25
+ import { resolveAgentRoot } from "./agent-root.mjs";
26
+
27
+ /**
28
+ * Resolve the process-locks directory.
29
+ *
30
+ * Resolved per call (not memoised at import) via the shared {@link resolveAgentRoot}
31
+ * so singleton honours the same AGENT_ROOT → AGENT_DIR → cwd precedence as the
32
+ * cadence bus, and so callers/tests that set the env after import are respected.
33
+ * Previously this read AGENT_ROOT once at import with no AGENT_DIR fallback,
34
+ * diverging from the bus and pinning the path to import-time cwd. (L5/L23)
35
+ *
36
+ * @returns {string} Absolute path to the process locks directory.
37
+ */
38
+ function locksDir() {
39
+ return join(resolveAgentRoot(), "state", "locks", "process");
40
+ }
41
+
42
+ /**
43
+ * Attempt to acquire a singleton lock for a named service.
44
+ * Returns true if the lock was acquired (no other instance running).
45
+ * Returns false if another instance is already running.
46
+ *
47
+ * Handles stale PID files (process died without cleanup).
48
+ *
49
+ * @param {string} name - Service name (e.g., "slack-events", "daemon", "huddle-server")
50
+ * @returns {boolean}
51
+ */
52
+ export function acquireLock(name) {
53
+ const dir = locksDir();
54
+ mkdirSync(dir, { recursive: true });
55
+ const pidFile = join(dir, `${name}.pid`);
56
+
57
+ // Atomic create-or-fail with O_EXCL ("wx"). The previous
58
+ // existsSync()-then-writeFileSync() was a check-then-act TOCTOU race: two
59
+ // services starting at the same instant could both pass the existsSync
60
+ // check and both "acquire" the lock, ending up with two daemons draining
61
+ // the same bus (double escalation, double side effects). With O_EXCL the
62
+ // kernel guarantees exactly one winner; the loser inspects the holder and
63
+ // reclaims only a genuinely stale lock, then retries once. (M9)
64
+ let acquired = false;
65
+ for (let attempt = 0; attempt < 2 && !acquired; attempt++) {
66
+ try {
67
+ const fd = openSync(pidFile, "wx");
68
+ // Store an identity token (process start time) alongside the PID so a
69
+ // future liveness check can distinguish "same process still alive" from
70
+ // "a different process happened to be assigned the same PID" — the
71
+ // classic PID-reuse-after-reboot false positive that would otherwise
72
+ // make a stale lock look alive and block startup forever. (L2)
73
+ writeSync(fd, serialiseHolder(process.pid, processIdentity(process.pid)));
74
+ closeSync(fd);
75
+ acquired = true;
76
+ } catch (err) {
77
+ if (err.code !== "EEXIST") throw err;
78
+ let holder = { pid: 0, identity: null };
79
+ try {
80
+ holder = parseHolder(readFileSync(pidFile, "utf8"));
81
+ } catch {
82
+ // Corrupt/unreadable PID file — treat as stale and reclaim below.
83
+ }
84
+ if (holder.pid && isHolderAlive(holder)) {
85
+ // Another instance is genuinely running.
86
+ return false;
87
+ }
88
+ // Stale (dead PID, recycled PID, or corrupt lock) — remove and retry once.
89
+ try { unlinkSync(pidFile); } catch { /* another start may have reclaimed it */ }
90
+ }
91
+ }
92
+ if (!acquired) return false;
93
+
94
+ // Register cleanup on exit
95
+ const cleanup = () => {
96
+ try { unlinkSync(pidFile); } catch { /* ignore */ }
97
+ };
98
+ process.on("exit", cleanup);
99
+ process.on("SIGINT", () => { cleanup(); process.exit(0); });
100
+ process.on("SIGTERM", () => { cleanup(); process.exit(0); });
101
+
102
+ return true;
103
+ }
104
+
105
+ /**
106
+ * Manually release a lock.
107
+ * @param {string} name
108
+ */
109
+ export function releaseLock(name) {
110
+ const pidFile = join(locksDir(), `${name}.pid`);
111
+ try { unlinkSync(pidFile); } catch { /* ignore */ }
112
+ }
113
+
114
+ /**
115
+ * Check if a service is currently running.
116
+ * @param {string} name
117
+ * @returns {{ running: boolean, pid: number|null }}
118
+ */
119
+ export function checkLock(name) {
120
+ const pidFile = join(locksDir(), `${name}.pid`);
121
+ if (!existsSync(pidFile)) return { running: false, pid: null };
122
+
123
+ try {
124
+ const holder = parseHolder(readFileSync(pidFile, "utf8"));
125
+ if (holder.pid && isHolderAlive(holder)) {
126
+ return { running: true, pid: holder.pid };
127
+ }
128
+ // Stale (dead PID, recycled PID, or corrupt lock) — clean up
129
+ try { unlinkSync(pidFile); } catch { /* ignore */ }
130
+ return { running: false, pid: null };
131
+ } catch {
132
+ return { running: false, pid: null };
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Best-effort process identity token: the process start time as reported by
138
+ * `ps`. Two different processes that happen to share a PID (e.g. PID reuse
139
+ * after a reboot) will almost never share a start time, so comparing this on
140
+ * the liveness check turns a PID-reuse false positive into a correctly-detected
141
+ * stale lock. Best-effort by design: if `ps` is unavailable or the PID is gone,
142
+ * returns null and the caller falls back to a plain existence check. (L2)
143
+ *
144
+ * @param {number} pid
145
+ * @returns {string|null}
146
+ */
147
+ function processIdentity(pid) {
148
+ if (!pid || pid <= 0) return null;
149
+ try {
150
+ // `ps -o lstart=` prints the process start time with no header. Stable for
151
+ // the life of the process; differs across PID reuse. execFileSync (not
152
+ // exec) avoids any shell. A non-existent PID exits non-zero -> caught.
153
+ const out = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
154
+ encoding: "utf8",
155
+ stdio: ["ignore", "pipe", "ignore"],
156
+ }).trim();
157
+ return out || null;
158
+ } catch {
159
+ // ps unavailable (or PID gone) — best effort, no identity captured.
160
+ return null;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Serialise a lock holder to the on-disk format. Backwards compatible: a bare
166
+ * PID (no identity) is still a valid first line, so old lock files written by
167
+ * earlier versions parse fine.
168
+ *
169
+ * Format: line 1 = PID, line 2 (optional) = identity token.
170
+ *
171
+ * @param {number} pid
172
+ * @param {string|null} identity
173
+ * @returns {string}
174
+ */
175
+ function serialiseHolder(pid, identity) {
176
+ return identity ? `${pid}\n${identity}` : String(pid);
177
+ }
178
+
179
+ /**
180
+ * Parse a lock file's contents into a holder record. Tolerates the legacy
181
+ * single-line (PID only) format as well as the PID + identity format.
182
+ *
183
+ * @param {string} raw
184
+ * @returns {{ pid: number, identity: string|null }}
185
+ */
186
+ function parseHolder(raw) {
187
+ const lines = String(raw).split("\n");
188
+ const pid = parseInt((lines[0] || "").trim(), 10) || 0;
189
+ const identity = lines.length > 1 ? (lines.slice(1).join("\n").trim() || null) : null;
190
+ return { pid, identity };
191
+ }
192
+
193
+ /**
194
+ * Liveness check for a recorded lock holder, identity-aware.
195
+ *
196
+ * 1. If the PID is not alive at all -> not alive (stale).
197
+ * 2. If the lock recorded no identity (legacy file or ps was unavailable when
198
+ * it was written) -> fall back to the plain PID existence result (no worse
199
+ * than the historical behaviour).
200
+ * 3. If an identity was recorded, recompute the current identity for that PID
201
+ * and require a match. A mismatch means the PID was recycled by a different
202
+ * process, so the lock is stale and reclaimable. If we cannot recompute the
203
+ * current identity (ps unavailable now), we err on the side of "alive" to
204
+ * avoid evicting a real holder.
205
+ *
206
+ * @param {{ pid: number, identity: string|null }} holder
207
+ * @returns {boolean}
208
+ */
209
+ function isHolderAlive(holder) {
210
+ if (!holder.pid || !isProcessRunning(holder.pid)) return false;
211
+ if (!holder.identity) return true; // legacy / no token recorded — best effort
212
+ const current = processIdentity(holder.pid);
213
+ if (current === null) return true; // can't verify now — don't evict a live holder
214
+ return current === holder.identity;
215
+ }
216
+
217
+ /**
218
+ * Check if a PID is still alive.
219
+ * @param {number} pid
220
+ * @returns {boolean}
221
+ */
222
+ function isProcessRunning(pid) {
223
+ try {
224
+ process.kill(pid, 0); // Signal 0 = existence check, no actual signal
225
+ return true;
226
+ } catch {
227
+ return false;
228
+ }
229
+ }
@@ -0,0 +1,135 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, writeFileSync, existsSync, rmSync, readFileSync, mkdirSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+
7
+ // singleton.js now resolves the locks dir per call from AGENT_ROOT (then
8
+ // AGENT_DIR, then cwd). Set AGENT_ROOT to a throwaway dir before importing so
9
+ // the default suite operates inside it; individual tests that exercise the
10
+ // AGENT_DIR fallback save/restore the environment themselves.
11
+ const root = mkdtempSync(join(tmpdir(), "maestro-singleton-"));
12
+ process.env.AGENT_ROOT = root;
13
+ delete process.env.AGENT_DIR;
14
+ const { acquireLock, releaseLock, checkLock } = await import("./singleton.js");
15
+
16
+ const LOCKS = join(root, "state", "locks", "process");
17
+
18
+ test("first acquire succeeds; second (live holder) is refused", () => {
19
+ assert.equal(acquireLock("a"), true);
20
+ // Our own PID is the holder and is alive, so re-acquire must fail.
21
+ assert.equal(acquireLock("a"), false);
22
+ releaseLock("a");
23
+ });
24
+
25
+ test("release frees the lock so it can be re-acquired", () => {
26
+ assert.equal(acquireLock("b"), true);
27
+ releaseLock("b");
28
+ assert.equal(acquireLock("b"), true);
29
+ releaseLock("b");
30
+ });
31
+
32
+ test("a stale lock (dead PID) is reclaimed atomically", () => {
33
+ const pidFile = join(LOCKS, "c.pid");
34
+ writeFileSync(pidFile, "2147483646"); // effectively never a live PID
35
+ assert.equal(acquireLock("c"), true);
36
+ // The reclaimed lock now records our PID on line 1 (identity may follow).
37
+ assert.equal(readFileSync(pidFile, "utf8").split("\n")[0].trim(), String(process.pid));
38
+ releaseLock("c");
39
+ });
40
+
41
+ test("a corrupt lock file is reclaimed", () => {
42
+ const pidFile = join(LOCKS, "d.pid");
43
+ writeFileSync(pidFile, "not-a-pid");
44
+ assert.equal(acquireLock("d"), true);
45
+ releaseLock("d");
46
+ });
47
+
48
+ test("acquire writes our own pid into the lock file", () => {
49
+ assert.equal(acquireLock("e"), true);
50
+ assert.equal(readFileSync(join(LOCKS, "e.pid"), "utf8").split("\n")[0].trim(), String(process.pid));
51
+ releaseLock("e");
52
+ assert.ok(!existsSync(join(LOCKS, "e.pid")));
53
+ });
54
+
55
+ test("checkLock reports running state for a held lock", () => {
56
+ assert.equal(acquireLock("f"), true);
57
+ const status = checkLock("f");
58
+ assert.equal(status.running, true);
59
+ assert.equal(status.pid, process.pid);
60
+ releaseLock("f");
61
+ assert.equal(checkLock("f").running, false);
62
+ });
63
+
64
+ // L5/L23: locks dir resolution honours AGENT_DIR when AGENT_ROOT is unset, and
65
+ // is computed per call rather than memoised at import.
66
+ test("locks dir honours AGENT_DIR when AGENT_ROOT is unset", () => {
67
+ const savedRoot = process.env.AGENT_ROOT;
68
+ const savedDir = process.env.AGENT_DIR;
69
+ const altRoot = mkdtempSync(join(tmpdir(), "maestro-singleton-agentdir-"));
70
+ try {
71
+ delete process.env.AGENT_ROOT;
72
+ process.env.AGENT_DIR = altRoot;
73
+ const altLocks = join(altRoot, "state", "locks", "process");
74
+
75
+ // Acquire under AGENT_DIR — the lock file must land under altRoot, proving
76
+ // per-call resolution picked up AGENT_DIR (not the import-time AGENT_ROOT).
77
+ assert.equal(acquireLock("agentdir"), true);
78
+ assert.ok(existsSync(join(altLocks, "agentdir.pid")));
79
+ // And it must NOT have been written under the original AGENT_ROOT tree.
80
+ assert.ok(!existsSync(join(LOCKS, "agentdir.pid")));
81
+ releaseLock("agentdir");
82
+ assert.ok(!existsSync(join(altLocks, "agentdir.pid")));
83
+ } finally {
84
+ if (savedRoot === undefined) delete process.env.AGENT_ROOT;
85
+ else process.env.AGENT_ROOT = savedRoot;
86
+ if (savedDir === undefined) delete process.env.AGENT_DIR;
87
+ else process.env.AGENT_DIR = savedDir;
88
+ rmSync(altRoot, { recursive: true, force: true });
89
+ }
90
+ });
91
+
92
+ // L2: PID-reuse false positive. A lock whose PID is alive (our own PID) but
93
+ // whose recorded identity token does NOT match the real process identity is a
94
+ // recycled PID, and must be reclaimed rather than treated as a live holder.
95
+ test("a stale lock with alive-but-different identity is reclaimed", () => {
96
+ const pidFile = join(LOCKS, "g.pid");
97
+ mkdirSync(LOCKS, { recursive: true });
98
+ // Our own PID (alive) + a bogus identity token that cannot match the real
99
+ // `ps -o lstart=` start time for this process.
100
+ writeFileSync(pidFile, `${process.pid}\nBOGUS-IDENTITY-TOKEN-NEVER-MATCHES`);
101
+
102
+ // checkLock must see this as not running (identity mismatch -> stale).
103
+ assert.equal(checkLock("g").running, false);
104
+
105
+ // And acquire must succeed by reclaiming the stale lock.
106
+ writeFileSync(pidFile, `${process.pid}\nBOGUS-IDENTITY-TOKEN-NEVER-MATCHES`);
107
+ assert.equal(acquireLock("g"), true);
108
+ // The reclaimed lock now records our PID on line 1.
109
+ assert.equal(readFileSync(pidFile, "utf8").split("\n")[0].trim(), String(process.pid));
110
+ releaseLock("g");
111
+ });
112
+
113
+ // Identity match path: a lock written by acquire (real identity recorded) for
114
+ // our own live PID is correctly treated as a live holder.
115
+ test("a lock with matching identity for a live PID is treated as held", () => {
116
+ assert.equal(acquireLock("h"), true);
117
+ // Re-acquire must fail: same PID, identity recomputed and matches.
118
+ assert.equal(acquireLock("h"), false);
119
+ assert.equal(checkLock("h").running, true);
120
+ releaseLock("h");
121
+ });
122
+
123
+ // Legacy compatibility: an old single-line (PID-only) lock for a live PID is
124
+ // still honoured as a live holder (no worse than historical behaviour).
125
+ test("a legacy pid-only lock for a live PID is honoured", () => {
126
+ const pidFile = join(LOCKS, "i.pid");
127
+ mkdirSync(LOCKS, { recursive: true });
128
+ writeFileSync(pidFile, String(process.pid)); // legacy format, no identity
129
+ assert.equal(checkLock("i").running, true);
130
+ // Re-acquire must be refused since the PID is alive and no identity was set.
131
+ assert.equal(acquireLock("i"), false);
132
+ rmSync(pidFile, { force: true });
133
+ });
134
+
135
+ test.after(() => rmSync(root, { recursive: true, force: true }));