@cohortapp/agent-sdk 2.11.15 → 2.13.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 (280) hide show
  1. package/.env.example +37 -22
  2. package/README.md +2 -0
  3. package/bin/maestro.mjs +117 -39
  4. package/bin/maestro.test.mjs +175 -5
  5. package/docs/guides/front-door-session.md +313 -0
  6. package/docs/guides/mac-mini.md +100 -28
  7. package/docs/guides/org-onboarding.md +1 -1
  8. package/docs/guides/setup-wizard.md +9 -5
  9. package/docs/runbooks/cohort-cutover.md +11 -1
  10. package/docs/runbooks/mac-mini-bootstrap.md +38 -63
  11. package/lib/cadence-bus-requeue.test.mjs +83 -0
  12. package/lib/cadence-bus.mjs +43 -7
  13. package/lib/channels/inbox-item.mjs +59 -2
  14. package/lib/cli/board.mjs +285 -0
  15. package/lib/cli/board.test.mjs +227 -0
  16. package/lib/cli/design.mjs +185 -0
  17. package/lib/cli/design.test.mjs +270 -0
  18. package/lib/cli/doctor-checks.mjs +441 -0
  19. package/lib/cli/doctor-checks.test.mjs +336 -0
  20. package/lib/cli/global-setup-extras.mjs +454 -0
  21. package/lib/cli/global-setup-extras.test.mjs +462 -0
  22. package/lib/cli/inbox.mjs +304 -0
  23. package/lib/cli/inbox.test.mjs +230 -0
  24. package/lib/cli/session-ack.mjs +63 -0
  25. package/lib/cli/session-ack.test.mjs +63 -0
  26. package/lib/cli/session.mjs +760 -0
  27. package/lib/cli/session.test.mjs +613 -0
  28. package/lib/collective/global-config.mjs +209 -6
  29. package/lib/collective/global-config.test.mjs +145 -0
  30. package/lib/collective/global-skills.mjs +145 -0
  31. package/lib/collective/global-skills.test.mjs +126 -0
  32. package/lib/collective/presence.mjs +4 -3
  33. package/lib/collective/vendor-skills.mjs +305 -0
  34. package/lib/collective/vendor-skills.test.mjs +306 -0
  35. package/lib/comms/send-gate.mjs +115 -0
  36. package/lib/comms/send-gate.test.mjs +113 -0
  37. package/lib/design/design-md.mjs +793 -0
  38. package/lib/design/design-md.test.mjs +318 -0
  39. package/lib/design/fixtures/DESIGN.golden.md +238 -0
  40. package/lib/design/fixtures/PRODUCT.golden.md +67 -0
  41. package/lib/design/fixtures/foundation.json +133 -0
  42. package/lib/design/refresh-gate.mjs +154 -0
  43. package/lib/design/refresh-gate.test.mjs +144 -0
  44. package/lib/design/write.mjs +275 -0
  45. package/lib/design/write.test.mjs +241 -0
  46. package/lib/feature-init.mjs +2 -2
  47. package/lib/mcp/server.test.mjs +9 -4
  48. package/lib/model-router/spawn.test.mjs +21 -0
  49. package/lib/org/board-mine-cache.mjs +99 -0
  50. package/lib/org/board-mine-cache.test.mjs +53 -0
  51. package/lib/org/board.mjs +11 -0
  52. package/lib/org/board.test.mjs +11 -1
  53. package/lib/org/client.mjs +36 -0
  54. package/lib/org/client.test.mjs +46 -0
  55. package/lib/org/inbound/directedness.mjs +18 -2
  56. package/lib/org/inbound/directedness.test.mjs +58 -0
  57. package/lib/org/inbound/index.mjs +8 -1
  58. package/lib/org/inbound/index.test.mjs +22 -0
  59. package/lib/org/mesh-directives.test.mjs +110 -0
  60. package/lib/org/mesh.mjs +61 -1
  61. package/lib/org/protocol.checksum +1 -1
  62. package/lib/org/protocol.mjs +52 -0
  63. package/lib/org/protocol.test.mjs +12 -1
  64. package/lib/org/registry.mjs +3 -2
  65. package/lib/org/tool-surface.mjs +120 -0
  66. package/lib/org/tool-surface.test.mjs +118 -5
  67. package/lib/prompts/parallelism.mjs +79 -0
  68. package/lib/prompts/parallelism.test.mjs +177 -0
  69. package/lib/security/external-content.mjs +1 -1
  70. package/lib/security/external-content.test.mjs +17 -0
  71. package/lib/session/config.mjs +137 -0
  72. package/lib/session/config.test.mjs +92 -0
  73. package/lib/session/feed-core.mjs +229 -0
  74. package/lib/session/feed-core.test.mjs +198 -0
  75. package/lib/session/first-run.mjs +126 -0
  76. package/lib/session/first-run.test.mjs +121 -0
  77. package/lib/session/frontdoor.mjs +266 -0
  78. package/lib/session/frontdoor.test.mjs +205 -0
  79. package/lib/session/handoffs.mjs +295 -0
  80. package/lib/session/handoffs.test.mjs +183 -0
  81. package/lib/session/identity.mjs +220 -0
  82. package/lib/session/identity.test.mjs +180 -0
  83. package/lib/session/inbox-claims.mjs +434 -0
  84. package/lib/session/inbox-claims.test.mjs +286 -0
  85. package/lib/session/launch-args.mjs +161 -0
  86. package/lib/session/launch-args.test.mjs +157 -0
  87. package/lib/session/liveness.mjs +174 -0
  88. package/lib/session/liveness.test.mjs +100 -0
  89. package/lib/session/status-summary.mjs +172 -0
  90. package/lib/session/status-summary.test.mjs +118 -0
  91. package/lib/session-permissions.mjs +39 -3
  92. package/lib/session-permissions.test.mjs +20 -0
  93. package/lib/setup/claude-probe.mjs +161 -24
  94. package/lib/setup/claude-probe.test.mjs +187 -0
  95. package/lib/setup/sections/learning.mjs +2 -1
  96. package/lib/setup/sections/model.mjs +104 -24
  97. package/lib/setup/sections/model.test.mjs +240 -0
  98. package/lib/setup/sections/org.mjs +27 -2
  99. package/lib/setup/sections/org.test.mjs +35 -2
  100. package/lib/setup/sections/verify.mjs +5 -0
  101. package/lib/setup/state.mjs +30 -10
  102. package/lib/setup/state.test.mjs +24 -1
  103. package/lib/singleton.js +11 -3
  104. package/lib/singleton.test.mjs +16 -0
  105. package/lib/subagents/lock.mjs +1 -1
  106. package/lib/telemetry/collect.mjs +270 -6
  107. package/lib/telemetry/collect.test.mjs +196 -1
  108. package/lib/upgrade/global-refresh.mjs +108 -0
  109. package/lib/upgrade/global-refresh.test.mjs +65 -0
  110. package/lib/upgrade/launchd-reconcile.mjs +327 -0
  111. package/lib/upgrade/launchd-reconcile.test.mjs +272 -0
  112. package/lib/upgrade/post-steps.mjs +151 -0
  113. package/lib/upgrade/post-steps.test.mjs +200 -0
  114. package/lib/upgrade/verify.mjs +215 -0
  115. package/lib/upgrade/verify.test.mjs +164 -0
  116. package/lib/voice/outbound.mjs +3 -2
  117. package/lib/voice/post-call-brief.mjs +2 -1
  118. package/lib/voice/session-rotation.mjs +6 -1
  119. package/lib/voice/session-rotation.test.mjs +114 -0
  120. package/package.json +3 -3
  121. package/plugins/maestro-skills/plugin.json +25 -1
  122. package/plugins/maestro-skills/skills/board-work.md +63 -0
  123. package/plugins/maestro-skills/skills/cohort-design.md +153 -0
  124. package/plugins/maestro-skills/skills/inbound-triage.md +80 -0
  125. package/plugins/maestro-skills/skills/main-session.md +102 -0
  126. package/plugins/maestro-skills/skills/peer-sessions.md +65 -0
  127. package/plugins/maestro-skills/skills/persona-discipline.md +75 -0
  128. package/plugins/maestro-skills/vendor/emilkowalski/LICENSE +21 -0
  129. package/plugins/maestro-skills/vendor/emilkowalski/UPSTREAM.json +70 -0
  130. package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/RECIPES.md +324 -0
  131. package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/SKILL.md +199 -0
  132. package/plugins/maestro-skills/vendor/emilkowalski/skills/animation-vocabulary/SKILL.md +173 -0
  133. package/plugins/maestro-skills/vendor/emilkowalski/skills/apple-design/SKILL.md +282 -0
  134. package/plugins/maestro-skills/vendor/emilkowalski/skills/emil-design-eng/SKILL.md +674 -0
  135. package/plugins/maestro-skills/vendor/emilkowalski/skills/find-animation-opportunities/SKILL.md +132 -0
  136. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/AUDIT.md +115 -0
  137. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/PLAN-TEMPLATE.md +73 -0
  138. package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/SKILL.md +101 -0
  139. package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/PICKER.md +197 -0
  140. package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/SKILL.md +90 -0
  141. package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/SKILL.md +112 -0
  142. package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/STANDARDS.md +187 -0
  143. package/plugins/maestro-skills/vendor/impeccable/LICENSE +191 -0
  144. package/plugins/maestro-skills/vendor/impeccable/NOTICE.md +11 -0
  145. package/plugins/maestro-skills/vendor/impeccable/SKILL.md +86 -0
  146. package/plugins/maestro-skills/vendor/impeccable/UPSTREAM.json +201 -0
  147. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-asset-producer.md +42 -0
  148. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-documenter.md +29 -0
  149. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-finish-reviewer.md +43 -0
  150. package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-manual-edit-applier.md +97 -0
  151. package/plugins/maestro-skills/vendor/impeccable/reference/adapt.md +312 -0
  152. package/plugins/maestro-skills/vendor/impeccable/reference/adapt.native.md +58 -0
  153. package/plugins/maestro-skills/vendor/impeccable/reference/android.md +46 -0
  154. package/plugins/maestro-skills/vendor/impeccable/reference/animate.md +89 -0
  155. package/plugins/maestro-skills/vendor/impeccable/reference/audit.md +136 -0
  156. package/plugins/maestro-skills/vendor/impeccable/reference/audit.native.md +139 -0
  157. package/plugins/maestro-skills/vendor/impeccable/reference/bolder.md +33 -0
  158. package/plugins/maestro-skills/vendor/impeccable/reference/clarify.md +94 -0
  159. package/plugins/maestro-skills/vendor/impeccable/reference/colorize.md +86 -0
  160. package/plugins/maestro-skills/vendor/impeccable/reference/craft-floor.md +44 -0
  161. package/plugins/maestro-skills/vendor/impeccable/reference/craft.md +5 -0
  162. package/plugins/maestro-skills/vendor/impeccable/reference/critique.md +806 -0
  163. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/asset-producer.md +37 -0
  164. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/documenter.md +24 -0
  165. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/finish-reviewer.md +38 -0
  166. package/plugins/maestro-skills/vendor/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  167. package/plugins/maestro-skills/vendor/impeccable/reference/delight.md +70 -0
  168. package/plugins/maestro-skills/vendor/impeccable/reference/distill.md +111 -0
  169. package/plugins/maestro-skills/vendor/impeccable/reference/doctor.md +54 -0
  170. package/plugins/maestro-skills/vendor/impeccable/reference/document.md +416 -0
  171. package/plugins/maestro-skills/vendor/impeccable/reference/extract.md +69 -0
  172. package/plugins/maestro-skills/vendor/impeccable/reference/harden.md +336 -0
  173. package/plugins/maestro-skills/vendor/impeccable/reference/hooks.md +111 -0
  174. package/plugins/maestro-skills/vendor/impeccable/reference/init.md +131 -0
  175. package/plugins/maestro-skills/vendor/impeccable/reference/ios.md +51 -0
  176. package/plugins/maestro-skills/vendor/impeccable/reference/layout.md +84 -0
  177. package/plugins/maestro-skills/vendor/impeccable/reference/live-setup.md +104 -0
  178. package/plugins/maestro-skills/vendor/impeccable/reference/live.md +325 -0
  179. package/plugins/maestro-skills/vendor/impeccable/reference/new-work.md +147 -0
  180. package/plugins/maestro-skills/vendor/impeccable/reference/onboard.md +234 -0
  181. package/plugins/maestro-skills/vendor/impeccable/reference/operate.md +61 -0
  182. package/plugins/maestro-skills/vendor/impeccable/reference/optimize.md +258 -0
  183. package/plugins/maestro-skills/vendor/impeccable/reference/overdrive.md +127 -0
  184. package/plugins/maestro-skills/vendor/impeccable/reference/polish.md +105 -0
  185. package/plugins/maestro-skills/vendor/impeccable/reference/quieter.md +99 -0
  186. package/plugins/maestro-skills/vendor/impeccable/reference/routing.md +24 -0
  187. package/plugins/maestro-skills/vendor/impeccable/reference/shape.md +59 -0
  188. package/plugins/maestro-skills/vendor/impeccable/reference/typeset.md +80 -0
  189. package/plugins/maestro-skills/vendor/impeccable/reference/visualize.md +46 -0
  190. package/plugins/maestro-skills/vendor/taste-skill/LICENSE +21 -0
  191. package/plugins/maestro-skills/vendor/taste-skill/UPSTREAM.json +37 -0
  192. package/plugins/maestro-skills/vendor/taste-skill/skills/minimalist-skill/SKILL.md +85 -0
  193. package/plugins/maestro-skills/vendor/taste-skill/skills/redesign-skill/SKILL.md +178 -0
  194. package/plugins/maestro-skills/vendor/taste-skill/skills/soft-skill/SKILL.md +98 -0
  195. package/plugins/maestro-skills/vendor/taste-skill/skills/taste-skill/SKILL.md +1206 -0
  196. package/plugins/maestro-skills/vendor/unlazy/LICENSE +21 -0
  197. package/plugins/maestro-skills/vendor/unlazy/SECURITY.md +72 -0
  198. package/plugins/maestro-skills/vendor/unlazy/SKILL.md +104 -0
  199. package/plugins/maestro-skills/vendor/unlazy/UPSTREAM.json +94 -0
  200. package/plugins/maestro-skills/vendor/unlazy/references/dispatch.md +82 -0
  201. package/plugins/maestro-skills/vendor/unlazy/references/gates.md +149 -0
  202. package/plugins/maestro-skills/vendor/unlazy/references/method.md +49 -0
  203. package/plugins/maestro-skills/vendor/unlazy/references/orchestration.md +107 -0
  204. package/plugins/maestro-skills/vendor/unlazy/references/parallel.md +133 -0
  205. package/plugins/maestro-skills/vendor/unlazy/references/token-economy.md +48 -0
  206. package/plugins/maestro-skills/vendor/unlazy/scripts/dispatch-check.mjs +139 -0
  207. package/plugins/maestro-skills/vendor/unlazy/scripts/gate-check.mjs +960 -0
  208. package/plugins/maestro-skills/vendor/unlazy/scripts/gate-lint.mjs +245 -0
  209. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/check-supervisor.mjs +46 -0
  210. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/dispatch.mjs +293 -0
  211. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/gates.mjs +953 -0
  212. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/process-tree.mjs +161 -0
  213. package/plugins/maestro-skills/vendor/unlazy/scripts/lib/regex-worker.mjs +9 -0
  214. package/plugins/maestro-skills/vendor/unlazy/templates/PLAN.md +116 -0
  215. package/plugins/maestro-skills/vendor/unlazy/templates/gates-leaf.md +51 -0
  216. package/plugins/maestro-skills/vendor/unlazy/templates/gates-node.md +51 -0
  217. package/scaffold/CLAUDE.md +24 -0
  218. package/scripts/ci/check-durable-write-seam.mjs +147 -0
  219. package/scripts/ci/check-durable-write-seam.test.mjs +90 -0
  220. package/scripts/ci/check-skill-packs.mjs +388 -0
  221. package/scripts/ci/check-skill-packs.test.mjs +495 -0
  222. package/scripts/ci/check.mjs +6 -0
  223. package/scripts/collective/hook-runner.mjs +39 -4
  224. package/scripts/collective/hook-runner.test.mjs +85 -2
  225. package/scripts/daemon/agent-daemon-board-mine.test.mjs +96 -0
  226. package/scripts/daemon/agent-daemon-design.test.mjs +238 -0
  227. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +60 -0
  228. package/scripts/daemon/agent-daemon.mjs +249 -10
  229. package/scripts/daemon/agent-daemon.test.mjs +73 -0
  230. package/scripts/daemon/assurance-e2e.test.mjs +141 -6
  231. package/scripts/daemon/assurance.mjs +461 -37
  232. package/scripts/daemon/assurance.test.mjs +408 -43
  233. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +393 -0
  234. package/scripts/daemon/cadence-consumer.mjs +289 -89
  235. package/scripts/daemon/cadence-handlers.mjs +53 -0
  236. package/scripts/daemon/classifier.mjs +1 -1
  237. package/scripts/daemon/dispatcher-resume.test.mjs +166 -0
  238. package/scripts/daemon/dispatcher.mjs +127 -19
  239. package/scripts/daemon/health.mjs +12 -1
  240. package/scripts/daemon/inbox-deferral-session.test.mjs +49 -0
  241. package/scripts/daemon/inbox-deferral.mjs +6 -0
  242. package/scripts/daemon/lib/self-echo.mjs +201 -0
  243. package/scripts/daemon/lib/self-echo.test.mjs +153 -0
  244. package/scripts/daemon/maestro-daemon.mjs +3 -0
  245. package/scripts/daemon/prompt-builder.mjs +19 -3
  246. package/scripts/daemon/responder.mjs +51 -40
  247. package/scripts/daemon/sdk-version.mjs +51 -0
  248. package/scripts/daemon/sdk-version.test.mjs +31 -0
  249. package/scripts/hooks/pre-send-audit.sh +97 -4
  250. package/scripts/hooks/pre-send-audit.test.mjs +140 -1
  251. package/scripts/local-triggers/autoupdate.sh +243 -19
  252. package/scripts/local-triggers/autoupdate.test.mjs +518 -0
  253. package/scripts/local-triggers/generate-plists.sh +24 -1
  254. package/scripts/local-triggers/generate-plists.test.mjs +49 -11
  255. package/scripts/org/send-orgmail.first-contact.test.mjs +102 -0
  256. package/scripts/org/send-orgmail.mjs +27 -3
  257. package/scripts/poller/inbox-privilege-injection.test.mjs +167 -0
  258. package/scripts/poller/slack-poller.mjs +13 -1
  259. package/scripts/poller/utils.mjs +46 -1
  260. package/scripts/poller-launchd/install.sh +19 -11
  261. package/scripts/poller-launchd/install.test.mjs +243 -0
  262. package/scripts/poller-launchd/launchd-poller-wrapper.sh +92 -0
  263. package/scripts/poller-launchd/migrate.sh +66 -0
  264. package/scripts/poller-launchd/poller.plist.template +4 -2
  265. package/scripts/session/feed.mjs +237 -0
  266. package/scripts/session/feed.test.mjs +196 -0
  267. package/scripts/session/supervisor-sh.test.mjs +218 -0
  268. package/scripts/session/supervisor.mjs +328 -0
  269. package/scripts/session/supervisor.sh +141 -0
  270. package/scripts/session/supervisor.test.mjs +482 -0
  271. package/scripts/setup/configure-macos.sh +250 -55
  272. package/scripts/setup/configure-macos.test.mjs +306 -0
  273. package/scripts/setup/init-agent.sh +112 -7
  274. package/scripts/setup/init-agent.test.mjs +220 -1
  275. package/scripts/vendor/skill-packs.mjs +354 -0
  276. package/scripts/vendor/sync-skill-packs.mjs +242 -0
  277. package/scripts/vendor/sync-skill-packs.test.mjs +103 -0
  278. package/scripts/watchdog/memory-watchdog.sh +37 -1
  279. package/scripts/watchdog/memory-watchdog.test.mjs +64 -0
  280. package/scripts/setup/boot-claude-session.sh +0 -94
@@ -0,0 +1,196 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync, utimesSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join, resolve, dirname, basename } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { spawnSync } from "node:child_process";
8
+
9
+ import { createFeed, parseFeedArgs } from "./feed.mjs";
10
+ import { inboxItemToYaml } from "../poller/utils.mjs";
11
+ import { newMainSession, saveMainSession } from "../../lib/session/identity.mjs";
12
+ import { sessionPaths } from "../../lib/session/config.mjs";
13
+ import { parseHeartbeat } from "../../lib/session/liveness.mjs";
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const FEED = resolve(__dirname, "feed.mjs");
17
+ const T0 = Date.parse("2026-09-08T10:00:00Z");
18
+ const UUID = "11111111-2222-4333-8444-555555555555";
19
+
20
+ function makeAgent() {
21
+ const root = mkdtempSync(join(tmpdir(), "maestro-feed-"));
22
+ mkdirSync(join(root, "config"), { recursive: true });
23
+ writeFileSync(join(root, "config", "agent.json"), JSON.stringify({ firstName: "Olivia" }));
24
+ saveMainSession(root, { ...newMainSession({ now: T0, uuid: () => UUID }), resumes: 1 });
25
+ return root;
26
+ }
27
+
28
+ function writeItem(root, id, over = {}) {
29
+ const dir = join(root, "state", "inbox", "cohort");
30
+ mkdirSync(dir, { recursive: true });
31
+ const p = join(dir, `2026-09-08T09-59-00-000Z-${id}.yaml`);
32
+ writeFileSync(p, inboxItemToYaml({
33
+ id, service: "cohort", channel: "dm/casey", channel_id: "ch_1", sender: "Casey", sender_privilege: "ceo",
34
+ timestamp: "2026-09-08T09:59:00.000Z", subject: "Cohort message", content: `hello ${id}`, thread_id: "", raw_ref: `cohort:ch_1:${id}`, ...over,
35
+ }));
36
+ return p;
37
+ }
38
+
39
+ test("parseFeedArgs", () => {
40
+ assert.deepEqual(parseFeedArgs([]), { once: false });
41
+ assert.deepEqual(parseFeedArgs(["--once"]), { once: true });
42
+ });
43
+
44
+ test("createFeed: tick emits new inbox items + handoffs + directives as objects, persists the seen-set, and beats", () => {
45
+ const root = makeAgent();
46
+ try {
47
+ const itemPath = writeItem(root, "m1");
48
+ const paths = sessionPaths(root);
49
+ mkdirSync(paths.handoffsDir, { recursive: true });
50
+ writeFileSync(join(paths.handoffsDir, "t-1.json"), JSON.stringify({ tickId: "t-1", cadence: "morning-brief", promptPath: "/p", enqueuedAt: "x" }));
51
+ writeFileSync(paths.restartRequestedFile, "");
52
+
53
+ const lines = [];
54
+ let t = T0;
55
+ const feed = createFeed({ agentRoot: root, config: { services: ["cohort"] }, now: () => t, emit: (o) => lines.push(o), pid: 4242, ppid: 4241, pidAlive: () => false });
56
+ feed.tick();
57
+ assert.deepEqual(lines.map((l) => [l.type, l.id || l.action]), [["inbound", "m1"], ["handoff", "t-1"], ["directive", "restart"], ["directive", "daemon-stale"]]);
58
+ const inbound = lines[0];
59
+ assert.equal(inbound.path, itemPath);
60
+ assert.equal(inbound.preview, "hello m1");
61
+ assert.equal(inbound.channelName, "dm/casey");
62
+ assert.ok(existsSync(paths.feedSeenFile));
63
+ const seen = JSON.parse(readFileSync(paths.feedSeenFile, "utf8"));
64
+ assert.equal(seen.seq, 4);
65
+
66
+ // Heartbeat carries the stable session id and the <first>-main name.
67
+ feed.beat();
68
+ const hb = parseHeartbeat(readFileSync(paths.heartbeatFile, "utf8"));
69
+ assert.equal(hb.pid, 4242);
70
+ assert.equal(hb.ppid, 4241);
71
+ assert.equal(hb.sessionId, UUID);
72
+ assert.equal(hb.name, "olivia-main");
73
+ assert.equal(hb.ts, "2026-09-08T10:00:00.000Z");
74
+
75
+ // Second tick: nothing new; a second feed instance (Monitor restart) replays nothing either.
76
+ t += 1000;
77
+ lines.length = 0;
78
+ feed.tick();
79
+ assert.deepEqual(lines, []);
80
+ const feed2 = createFeed({ agentRoot: root, config: { services: ["cohort"] }, now: () => t, emit: (o) => lines.push(o), pidAlive: () => false });
81
+ feed2.tick();
82
+ assert.deepEqual(lines, []);
83
+
84
+ // A new item and a daemon coming alive (fresh health file) — only the item is emitted.
85
+ writeItem(root, "m2");
86
+ mkdirSync(dirname(paths.daemonHealthFile), { recursive: true });
87
+ writeFileSync(paths.daemonHealthFile, "updated: now\n");
88
+ utimesSync(paths.daemonHealthFile, new Date(t), new Date(t));
89
+ feed2.tick();
90
+ assert.deepEqual(lines.map((l) => [l.type, l.id]), [["inbound", "m2"]]);
91
+ assert.equal(lines[0].seq, 5);
92
+ } finally {
93
+ rmSync(root, { recursive: true, force: true });
94
+ }
95
+ });
96
+
97
+ test("createFeed: claimed (.dispatched) and processed items are not emitted; a re-opened item is", () => {
98
+ const root = makeAgent();
99
+ try {
100
+ const p = writeItem(root, "m1");
101
+ const lines = [];
102
+ const feed = createFeed({ agentRoot: root, config: { services: ["cohort"] }, now: () => T0, emit: (o) => lines.push(o), pidAlive: () => true });
103
+ feed.tick();
104
+ assert.equal(lines.filter((l) => l.type === "inbound").length, 1);
105
+ // Claimed by the session → renamed; nothing re-emitted.
106
+ const claimed = p + ".dispatched";
107
+ writeFileSync(claimed, readFileSync(p));
108
+ rmSync(p);
109
+ lines.length = 0;
110
+ feed.tick();
111
+ assert.deepEqual(lines, []);
112
+ // Assurance sweep re-opens it → re-emitted once.
113
+ writeFileSync(p, readFileSync(claimed));
114
+ rmSync(claimed);
115
+ feed.tick();
116
+ assert.equal(lines.length, 1);
117
+ assert.equal(lines[0].id, "m1");
118
+ } finally {
119
+ rmSync(root, { recursive: true, force: true });
120
+ }
121
+ });
122
+
123
+ test("createFeed: an already-seen inbox item is stat-only — its body is not re-read every tick (O(backlog) reads)", () => {
124
+ const root = makeAgent();
125
+ try {
126
+ for (let i = 0; i < 5; i++) writeItem(root, `m${i}`);
127
+ const reads = [];
128
+ const fs = { readFileSync: (p, enc) => { reads.push(String(p)); return readFileSync(p, enc); } };
129
+ const lines = [];
130
+ const feed = createFeed({ agentRoot: root, config: { services: ["cohort"] }, now: () => T0, emit: (o) => lines.push(o), pidAlive: () => true, fs });
131
+ feed.tick();
132
+ assert.equal(lines.filter((l) => l.type === "inbound").length, 5);
133
+ const firstPassBodies = reads.filter((p) => p.includes("/state/inbox/cohort/")).length;
134
+ assert.equal(firstPassBodies, 5);
135
+ reads.length = 0;
136
+ feed.tick();
137
+ feed.tick();
138
+ assert.equal(reads.filter((p) => p.includes("/state/inbox/cohort/")).length, 0, "seen items must not be read again");
139
+ // A new item lands: only it is read.
140
+ writeItem(root, "m9");
141
+ reads.length = 0;
142
+ feed.tick();
143
+ assert.deepEqual(reads.filter((p) => p.includes("/state/inbox/cohort/")).map((p) => basename(p)), ["2026-09-08T09-59-00-000Z-m9.yaml"]);
144
+ assert.equal(lines.filter((l) => l.type === "inbound").length, 6);
145
+ } finally {
146
+ rmSync(root, { recursive: true, force: true });
147
+ }
148
+ });
149
+
150
+ test("createFeed: tickSafely never throws — a throwing emitter (closed stdout) costs one tick and a stderr line", () => {
151
+ const root = makeAgent();
152
+ try {
153
+ writeItem(root, "m1");
154
+ const logs = [];
155
+ const feed = createFeed({ agentRoot: root, config: { services: ["cohort"] }, now: () => T0, emit: () => { throw new Error("EPIPE"); }, log: (l) => logs.push(l), pidAlive: () => true });
156
+ assert.throws(() => feed.tick(), /EPIPE/);
157
+ assert.deepEqual(feed.tickSafely(), []);
158
+ assert.ok(logs.some((l) => /tick failed/.test(l) && /EPIPE/.test(l)));
159
+ } finally {
160
+ rmSync(root, { recursive: true, force: true });
161
+ }
162
+ });
163
+
164
+ test("createFeed: a missing inbox dir, a missing daemon, a missing main-session file — all fail open", () => {
165
+ const root = mkdtempSync(join(tmpdir(), "maestro-feed-bare-"));
166
+ try {
167
+ const lines = [];
168
+ const feed = createFeed({ agentRoot: root, config: { services: ["cohort", "slack"] }, now: () => T0, emit: (o) => lines.push(o), pidAlive: () => false });
169
+ assert.doesNotThrow(() => feed.tick());
170
+ assert.deepEqual(lines.map((l) => l.action), ["daemon-stale"]);
171
+ assert.deepEqual(feed.beat(), { ok: true });
172
+ const hb = parseHeartbeat(readFileSync(sessionPaths(root).heartbeatFile, "utf8"));
173
+ assert.equal(hb.sessionId, null);
174
+ // No config at all → the directory-name derivation the plist generator uses.
175
+ assert.equal(hb.name, `${basename(root)}-main`);
176
+ } finally {
177
+ rmSync(root, { recursive: true, force: true });
178
+ }
179
+ });
180
+
181
+ test("feed.mjs --once: stdout is JSON lines only, nothing else", () => {
182
+ const root = makeAgent();
183
+ try {
184
+ writeItem(root, "m1");
185
+ const r = spawnSync(process.execPath, [FEED, "--once"], { env: { ...process.env, AGENT_ROOT: root, AGENT_DIR: root }, encoding: "utf8", timeout: 20_000 });
186
+ assert.equal(r.status, 0, r.stderr);
187
+ const lines = r.stdout.split("\n").filter(Boolean);
188
+ assert.ok(lines.length >= 1, "expected at least the inbound line");
189
+ const parsed = lines.map((l) => JSON.parse(l));
190
+ assert.equal(parsed[0].type, "inbound");
191
+ assert.equal(parsed[0].id, "m1");
192
+ assert.ok(existsSync(sessionPaths(root).heartbeatFile), "heartbeat written by --once");
193
+ } finally {
194
+ rmSync(root, { recursive: true, force: true });
195
+ }
196
+ });
@@ -0,0 +1,218 @@
1
+ /**
2
+ * supervisor-sh.test.mjs — coverage for scripts/session/supervisor.sh.
3
+ *
4
+ * Builds a fixture agent root, points NODE_BIN at a stub that dumps its argv
5
+ * and environment, runs the wrapper, and asserts what reached the "node"
6
+ * process: AGENT_ROOT, HOME, the PATH prefix, the .env variables (quotes
7
+ * stripped, malformed lines skipped, existing env not clobbered), and the
8
+ * supervisor.mjs entrypoint. Also pins that the wrapper's exit code is the
9
+ * child's (exec) and that the log lands under logs/session/.
10
+ */
11
+
12
+ import { test } from "node:test";
13
+ import assert from "node:assert/strict";
14
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync, copyFileSync, chmodSync, rmSync, realpathSync, existsSync } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { join, resolve, dirname } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { spawnSync } from "node:child_process";
19
+
20
+ const __dirname = dirname(fileURLToPath(import.meta.url));
21
+ const WRAPPER_SRC = resolve(__dirname, "supervisor.sh");
22
+
23
+ function makeAgent() {
24
+ // The wrapper resolves AGENT_ROOT with `pwd -P`, so compare against the real path (/var → /private/var on macOS).
25
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "maestro-supervisor-sh-")));
26
+ mkdirSync(join(root, "scripts", "session"), { recursive: true });
27
+ mkdirSync(join(root, "config"), { recursive: true });
28
+ writeFileSync(join(root, "config", "agent.json"), JSON.stringify({ firstName: "Olivia" }));
29
+ copyFileSync(WRAPPER_SRC, join(root, "scripts", "session", "supervisor.sh"));
30
+ chmodSync(join(root, "scripts", "session", "supervisor.sh"), 0o755);
31
+ writeFileSync(join(root, "scripts", "session", "supervisor.mjs"), "// stub entrypoint (never run: NODE_BIN is a stub)\n");
32
+ mkdirSync(join(root, "bin"), { recursive: true });
33
+ const stub = join(root, "bin", "node");
34
+ writeFileSync(stub, [
35
+ "#!/bin/sh",
36
+ 'echo "ARGV:$*"',
37
+ "env | grep -E '^(AGENT_ROOT|AGENT_DIR|HOME|USER|PATH|MAESTRO_TEST_[A-Z_]*)=' | sort",
38
+ 'exit "${STUB_EXIT:-0}"',
39
+ "",
40
+ ].join("\n"));
41
+ chmodSync(stub, 0o755);
42
+ return { root, stub };
43
+ }
44
+
45
+ function runWrapper(root, stub, extraEnv = {}) {
46
+ const env = { PATH: "/usr/bin:/bin", NODE_BIN: stub, ...extraEnv };
47
+ const r = spawnSync("/bin/bash", [join(root, "scripts", "session", "supervisor.sh"), "--flag", "x"], { env, encoding: "utf8", timeout: 20_000 });
48
+ const logDir = join(root, "logs", "session");
49
+ let log = "";
50
+ try {
51
+ const files = readdirSync(logDir).filter((f) => /^supervisor-\d{4}-\d{2}-\d{2}\.log$/.test(f));
52
+ log = files.map((f) => readFileSync(join(logDir, f), "utf8")).join("");
53
+ } catch { /* no log dir → assertions below fail loudly */ }
54
+ return { r, log };
55
+ }
56
+
57
+ test("supervisor.sh parses under bash -n", () => {
58
+ const r = spawnSync("/bin/bash", ["-n", WRAPPER_SRC], { encoding: "utf8" });
59
+ assert.equal(r.status, 0, r.stderr);
60
+ });
61
+
62
+ test("wrapper sets AGENT_ROOT/HOME/PATH, sources .env safely, execs supervisor.mjs and logs under logs/session", () => {
63
+ const { root, stub } = makeAgent();
64
+ try {
65
+ writeFileSync(join(root, ".env"), [
66
+ "# comment line",
67
+ "",
68
+ "MAESTRO_TEST_PLAIN=plain",
69
+ 'MAESTRO_TEST_QUOTED="has spaces and = signs"',
70
+ "MAESTRO_TEST_SINGLE='single quoted'",
71
+ "export MAESTRO_TEST_EXPORTED=exported",
72
+ "MAESTRO_TEST_PRESET=from-dotenv",
73
+ "this line is not a kv pair",
74
+ "BAD KEY=nope",
75
+ "MAESTRO_TEST_BAD-DASH=nope",
76
+ // dotenv semantics the daemon already relies on: an unquoted value ends
77
+ // at the first `#` and is trimmed; a quoted value keeps its `#`; a CRLF
78
+ // line (edited on Windows) must not leak a trailing \r into a token.
79
+ "MAESTRO_TEST_INLINE=value # trailing comment",
80
+ "MAESTRO_TEST_NOSPACE=abc#def",
81
+ "MAESTRO_TEST_TRAIL=spaced ",
82
+ 'MAESTRO_TEST_QHASH="keep # this"',
83
+ "MAESTRO_TEST_CRLF=windows\r",
84
+ "MAESTRO_TEST_CRLFQ=\"quoted windows\"\r",
85
+ "",
86
+ ].join("\n"));
87
+ const { r, log } = runWrapper(root, stub, { MAESTRO_TEST_PRESET: "from-shell", HOME: "/Users/fixture" });
88
+ assert.equal(r.status, 0, `${r.stdout}\n${r.stderr}`);
89
+ assert.equal(r.stdout, "", "wrapper must not write to its own stdout (everything goes to the log)");
90
+ // exec'd entrypoint + passthrough args
91
+ assert.match(log, new RegExp(`^ARGV:${join(root, "scripts", "session", "supervisor.mjs").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")} --flag x$`, "m"));
92
+ assert.match(log, new RegExp(`^AGENT_ROOT=${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m"));
93
+ assert.match(log, new RegExp(`^AGENT_DIR=${root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`, "m"));
94
+ assert.match(log, /^HOME=\/Users\/fixture$/m);
95
+ assert.match(log, /^USER=.+$/m);
96
+ assert.match(log, /^PATH=\/Users\/fixture\/\.local\/bin:\/opt\/homebrew\/bin:\/opt\/homebrew\/sbin:\/usr\/local\/bin:\/usr\/bin:\/bin:\/usr\/sbin:\/sbin:\/usr\/bin:\/bin$/m);
97
+ // .env semantics
98
+ assert.match(log, /^MAESTRO_TEST_PLAIN=plain$/m);
99
+ assert.match(log, /^MAESTRO_TEST_QUOTED=has spaces and = signs$/m);
100
+ assert.match(log, /^MAESTRO_TEST_SINGLE=single quoted$/m);
101
+ assert.match(log, /^MAESTRO_TEST_EXPORTED=exported$/m);
102
+ assert.match(log, /^MAESTRO_TEST_PRESET=from-shell$/m, "an existing env var must win over .env");
103
+ assert.doesNotMatch(log, /MAESTRO_TEST_BAD/);
104
+ assert.doesNotMatch(log, /nope/);
105
+ assert.match(log, /^MAESTRO_TEST_INLINE=value$/m, "unquoted inline comment must be stripped");
106
+ assert.match(log, /^MAESTRO_TEST_NOSPACE=abc$/m);
107
+ assert.match(log, /^MAESTRO_TEST_TRAIL=spaced$/m);
108
+ assert.match(log, /^MAESTRO_TEST_QHASH=keep # this$/m, "a quoted # is data");
109
+ assert.match(log, /^MAESTRO_TEST_CRLF=windows$/m, "CR from a CRLF line must not reach the value");
110
+ assert.match(log, /^MAESTRO_TEST_CRLFQ=quoted windows$/m);
111
+ assert.doesNotMatch(log, /\r/, "no carriage return anywhere in the child env");
112
+ } finally {
113
+ rmSync(root, { recursive: true, force: true });
114
+ }
115
+ });
116
+
117
+ test("wrapper works with no .env at all and returns the supervisor's exit code (75)", () => {
118
+ const { root, stub } = makeAgent();
119
+ try {
120
+ const { r, log } = runWrapper(root, stub, { STUB_EXIT: "75" });
121
+ assert.equal(r.status, 75);
122
+ assert.match(log, /^ARGV:/m);
123
+ } finally {
124
+ rmSync(root, { recursive: true, force: true });
125
+ }
126
+ });
127
+
128
+ test("under an emergency stop (.emergency-stop or state/EMERGENCY_STOP) the wrapper exits 0 without starting node — launchd's KeepAlive does not relaunch a clean exit", () => {
129
+ for (const marker of [".emergency-stop", join("state", "EMERGENCY_STOP")]) {
130
+ const { root, stub } = makeAgent();
131
+ try {
132
+ mkdirSync(join(root, "state"), { recursive: true });
133
+ writeFileSync(join(root, marker), "2026-09-09T00:00:00Z\n");
134
+ const { r, log } = runWrapper(root, stub);
135
+ assert.equal(r.status, 0, `${marker}: ${r.stderr}`);
136
+ assert.match(r.stderr, /emergency stop is active/, marker);
137
+ assert.match(r.stderr, /maestro session start/);
138
+ assert.ok(!/ARGV:/.test(log), `${marker}: node must not be started`);
139
+ } finally {
140
+ rmSync(root, { recursive: true, force: true });
141
+ }
142
+ }
143
+ });
144
+
145
+ test("wrapper exits 127 when no node can be found (candidate list overridden so the branch is deterministic)", () => {
146
+ const { root } = makeAgent();
147
+ try {
148
+ const r = spawnSync("/bin/bash", [join(root, "scripts", "session", "supervisor.sh")], {
149
+ env: {
150
+ PATH: "/usr/bin:/bin",
151
+ HOME: join(root, "nohome"), // no ~/.nvm here either
152
+ NODE_BIN: "/nonexistent/node",
153
+ MAESTRO_NODE_CANDIDATES: "/nonexistent/a/node:/nonexistent/b/node",
154
+ },
155
+ encoding: "utf8",
156
+ timeout: 20_000,
157
+ });
158
+ assert.equal(r.status, 127, `${r.stdout}\n${r.stderr}`);
159
+ assert.match(r.stderr, /could not find node/);
160
+ } finally {
161
+ rmSync(root, { recursive: true, force: true });
162
+ }
163
+ });
164
+
165
+ test("MAESTRO_NODE_CANDIDATES: the first executable candidate wins", () => {
166
+ const { root, stub } = makeAgent();
167
+ try {
168
+ const r = spawnSync("/bin/bash", [join(root, "scripts", "session", "supervisor.sh")], {
169
+ env: { PATH: "/usr/bin:/bin", HOME: join(root, "nohome"), MAESTRO_NODE_CANDIDATES: `/nonexistent/node:${stub}` },
170
+ encoding: "utf8",
171
+ timeout: 20_000,
172
+ });
173
+ assert.equal(r.status, 0, r.stderr);
174
+ const log = readdirSync(join(root, "logs", "session")).map((f) => readFileSync(join(root, "logs", "session", f), "utf8")).join("");
175
+ assert.match(log, /^ARGV:/m);
176
+ } finally {
177
+ rmSync(root, { recursive: true, force: true });
178
+ }
179
+ });
180
+
181
+ test("a preset AGENT_ROOT wins over the script's own location (the npm-global copy can serve an agent dir)", () => {
182
+ const pkg = makeAgent(); // where the wrapper lives (stands in for the global package)
183
+ const agent = makeAgent(); // the seat
184
+ try {
185
+ const r = spawnSync("/bin/bash", [join(pkg.root, "scripts", "session", "supervisor.sh")], {
186
+ env: { PATH: "/usr/bin:/bin", NODE_BIN: pkg.stub, AGENT_ROOT: agent.root },
187
+ encoding: "utf8",
188
+ timeout: 20_000,
189
+ });
190
+ assert.equal(r.status, 0, r.stderr);
191
+ const logDir = join(agent.root, "logs", "session");
192
+ const log = readdirSync(logDir).map((f) => readFileSync(join(logDir, f), "utf8")).join("");
193
+ const esc = (x) => x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
194
+ assert.match(log, new RegExp(`^AGENT_ROOT=${esc(agent.root)}$`, "m"));
195
+ assert.match(log, new RegExp(`^ARGV:${esc(join(agent.root, "scripts", "session", "supervisor.mjs"))}$`, "m"));
196
+ assert.ok(!existsSync(join(pkg.root, "logs")), "nothing is written under the package dir");
197
+ } finally {
198
+ rmSync(pkg.root, { recursive: true, force: true });
199
+ rmSync(agent.root, { recursive: true, force: true });
200
+ }
201
+ });
202
+
203
+ test("wrapper refuses (exit 78, EX_CONFIG) when the resolved root is not an agent dir (no config/agent.json)", () => {
204
+ const { root, stub } = makeAgent();
205
+ try {
206
+ rmSync(join(root, "config", "agent.json"));
207
+ const r = spawnSync("/bin/bash", [join(root, "scripts", "session", "supervisor.sh")], {
208
+ env: { PATH: "/usr/bin:/bin", NODE_BIN: stub },
209
+ encoding: "utf8",
210
+ timeout: 20_000,
211
+ });
212
+ assert.equal(r.status, 78, `${r.stdout}\n${r.stderr}`);
213
+ assert.match(r.stderr, /not an agent directory/);
214
+ assert.ok(!existsSync(join(root, "logs")), "must not create logs/ or run node in a non-agent dir");
215
+ } finally {
216
+ rmSync(root, { recursive: true, force: true });
217
+ }
218
+ });