@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
@@ -47,6 +47,17 @@ const AGENT_DIR = mkdtempSync(join(tmpdir(), "maestro-assurance-e2e-"));
47
47
  process.env.AGENT_DIR = AGENT_DIR;
48
48
  process.env.AGENT_ROOT = AGENT_DIR;
49
49
  process.env.MAESTRO_DAEMON_NO_AUTOSTART = "1";
50
+ // HERMETICITY. `receipts.recordOutbound` attributes a send to an obligation and
51
+ // a session from AMBIENT ENV when the caller does not name them — the seam that
52
+ // lets a CLI send lane inside a spawned session attribute itself. Run this file
53
+ // from inside a live maestro session and those variables are inherited, so the
54
+ // receipt SCENARIO 2 writes to prove "the session answered" is stamped with a
55
+ // DIFFERENT debt's key and reads back as `attributed-elsewhere`. The scenario
56
+ // then fails on a real machine and passes on a bare CI runner, which is the
57
+ // wrong way round for a test to be environment-sensitive.
58
+ delete process.env.MAESTRO_OBLIGATION_KEY;
59
+ delete process.env.MAESTRO_SESSION_ID;
60
+ delete process.env.AGENT_SESSION_ID;
50
61
 
51
62
  let daemon, assurance, receipts;
52
63
 
@@ -103,10 +114,16 @@ function transport(virtualClock = () => null) {
103
114
  };
104
115
  }
105
116
 
106
- /** The daemon's ack seam, shaped exactly like responder.sendHoldingMessage. */
107
- function ackSeam() {
117
+ /** The daemon's ack seam, shaped exactly like responder.sendHoldingMessage:
118
+ * the line is GENERATED (here through the REAL generateAck with an injected
119
+ * runner — no spawn in tests), and a failed generation means silence, never
120
+ * a canned fallback. */
121
+ function ackSeam(runImpl) {
108
122
  return async (item, classResult) => {
109
- const text = assurance.composeAck(item, classResult);
123
+ const text = await assurance.generateAck(item, classResult, {
124
+ runImpl: runImpl || (async () => "Taking the git fixes now — push coming shortly."),
125
+ });
126
+ if (text == null) return { sent: false, holdingText: null, error: "ack_generation_failed", genFailed: true };
110
127
  heard("ack", text);
111
128
  return { sent: true, holdingText: text, via: "fake" };
112
129
  };
@@ -136,6 +153,24 @@ function makeItem(id, content) {
136
153
  return item;
137
154
  }
138
155
 
156
+ /**
157
+ * Every scenario injects both seams that would otherwise spawn: `dispatch` (no
158
+ * child process) and `sendHoldingMessage` (the ack line comes from an injected
159
+ * runner). No scenario in this file ever needs the real `claude` binary.
160
+ *
161
+ * The daemon's ack path is nevertheless gated on `claudeAvailable()` — a
162
+ * NECESSARY production fail-safe (never post "on it" when the CLI that would
163
+ * answer cannot start). Left to the real probe, that gate reads a fact about
164
+ * the MACHINE, not about the scenario: these tests pass on a developer box with
165
+ * the CLI installed and suppress every acknowledgement on a CI runner without
166
+ * it. That is precisely how SCENARIOS 2, 3 and 6 came to be red on main and
167
+ * green everywhere they were written. The premise is declared here instead.
168
+ *
169
+ * The gate's false branch is not untested by this — see "the ack is suppressed
170
+ * when the CLI is unavailable" at the end of this file.
171
+ */
172
+ const CLI_PRESENT = () => true;
173
+
139
174
  const COMPLEX = {
140
175
  priority: "critical", action: "respond", model: "opus", category: "action_required",
141
176
  directed_at_agent: true, summary: "Fix the issues you just noted and push the fixes to git",
@@ -152,6 +187,7 @@ test("SCENARIO 1 — a fast ask gets a direct answer, with no pointless holding
152
187
 
153
188
  let holdingCalls = 0;
154
189
  await daemon.processItem(item, "cohort", {
190
+ claudeAvailable: CLI_PRESENT,
155
191
  classify: async () => ({ priority: "normal", action: "respond", model: "sonnet", category: "question", directed_at_agent: true, summary: "board call time" }),
156
192
  isQuickReply: () => true,
157
193
  sendQuickResponse: async (it) => {
@@ -181,6 +217,7 @@ test("SCENARIO 2 — a slow ask is acknowledged at once, updated while it runs,
181
217
 
182
218
  let onCloseHook = null;
183
219
  await daemon.processItem(item, "cohort", {
220
+ claudeAvailable: CLI_PRESENT,
184
221
  classify: async () => COMPLEX,
185
222
  isQuickReply: () => false,
186
223
  sendHoldingMessage: ackSeam(),
@@ -189,9 +226,10 @@ test("SCENARIO 2 — a slow ask is acknowledged at once, updated while it runs,
189
226
 
190
227
  const ack = transcript.find((m) => m.kind === "ack");
191
228
  assert.ok(ack, "the acknowledgement must go out");
192
- assert.ok(ack.ms < 1000, `acknowledged in ${ack.ms}ms — the old path budgeted 60,000ms and missed 2 times in 3`);
193
- assert.match(ack.text, /on it|looking now|digging in/i);
194
- assert.ok(ack.text.length <= 40, "the ack is one short human line, not a templated paragraph");
229
+ assert.ok(ack.ms < 1000, `acknowledged in ${ack.ms}ms`);
230
+ assert.match(ack.text, /git fixes/i, "the ack is bespoke to the actual ask, not a rotation pick");
231
+ assert.doesNotMatch(ack.text, /^(on it|looking now)[.!—]?\s*$/i, "never the retired canned lines");
232
+ assert.ok(ack.text.length <= 120, "the ack is one short human line, not a templated paragraph");
195
233
 
196
234
  const key = assurance.openObligations()[0].key;
197
235
  note("obligation opened and acknowledged; session running");
@@ -230,6 +268,7 @@ test("SCENARIO 3 — a timing-out session tells the human what happened and that
230
268
 
231
269
  let onCloseHook = null;
232
270
  await daemon.processItem(item, "cohort", {
271
+ claudeAvailable: CLI_PRESENT,
233
272
  classify: async () => ({ ...COMPLEX, summary: "Reconcile July spend against forecast" }),
234
273
  isQuickReply: () => false,
235
274
  sendHoldingMessage: ackSeam(),
@@ -265,6 +304,7 @@ test("SCENARIO 4 — a crashing session tells the human it failed, stops, and es
265
304
 
266
305
  let onCloseHook = null;
267
306
  await daemon.processItem(item, "cohort", {
307
+ claudeAvailable: CLI_PRESENT,
268
308
  classify: async () => ({ ...COMPLEX, summary: "Draft the Q3 investor update" }),
269
309
  isQuickReply: () => false,
270
310
  sendHoldingMessage: ackSeam(),
@@ -299,6 +339,7 @@ test("SCENARIO 5 — a session that exits 0 without replying no longer passes as
299
339
 
300
340
  let onCloseHook = null;
301
341
  await daemon.processItem(item, "cohort", {
342
+ claudeAvailable: CLI_PRESENT,
302
343
  classify: async () => ({ ...COMPLEX, summary: "Check whether the migration landed cleanly" }),
303
344
  isQuickReply: () => false,
304
345
  sendHoldingMessage: ackSeam(),
@@ -333,6 +374,7 @@ test("SCENARIO 6 — a debt orphaned by a daemon restart is spoken to on the nex
333
374
  const item = makeItem("ORPHAN-1", ask);
334
375
 
335
376
  await daemon.processItem(item, "cohort", {
377
+ claudeAvailable: CLI_PRESENT,
336
378
  classify: async () => ({ ...COMPLEX, summary: "Fix DNS on the staging domain" }),
337
379
  isQuickReply: () => false,
338
380
  sendHoldingMessage: ackSeam(),
@@ -357,6 +399,65 @@ test("SCENARIO 6 — a debt orphaned by a daemon restart is spoken to on the nex
357
399
  note("previously: the typing dot stopped and that was the last the human ever heard", 30_000);
358
400
  });
359
401
 
402
+ // ===========================================================================
403
+ // SCENARIO 6b — A PEER AGENT'S ACK ARRIVES AS INBOUND. The dispatch gate reads
404
+ // it, and NEITHER an ack goes out NOR a session spawns — the machinery half of
405
+ // the 25/27/30 Aug loop, closed at the gate.
406
+ // ===========================================================================
407
+
408
+ test("SCENARIO 6b — an ack-shaped one-liner from a peer agent seat spawns nothing and acks nothing", async () => {
409
+ const echoAck = "On it — taking a look now.";
410
+ beginScenario("SCENARIO 6b — PEER AGENT ACK (emitter-class inbound)", echoAck);
411
+ const item = {
412
+ id: "PEER-ACK-1",
413
+ raw_ref: "cohort:dm-a016-a039:PEER-ACK-1",
414
+ message_id: "PEER-ACK-1",
415
+ service: "cohort",
416
+ channel: "dm-a016-a039", // an agent↔agent DM — the structural seat signal
417
+ channel_id: "CDM-PEER-ACK-1",
418
+ is_dm: true,
419
+ sender: "Piotr Novak",
420
+ sender_id: "A039",
421
+ content: echoAck,
422
+ };
423
+ writeFileSync(join(AGENT_DIR, "state", "inbox", "cohort", "PEER-ACK-1.yaml"), "id: PEER-ACK-1\n");
424
+
425
+ let holdingCalls = 0;
426
+ let dispatched = 0;
427
+ await daemon.processItem(item, "cohort", {
428
+ claudeAvailable: CLI_PRESENT,
429
+ classify: async () => ({ ...COMPLEX, summary: "peer says it's on it" }),
430
+ isQuickReply: () => false,
431
+ sendHoldingMessage: async () => { holdingCalls++; return { sent: true, holdingText: "x" }; },
432
+ dispatch: () => { dispatched++; },
433
+ });
434
+
435
+ assert.equal(holdingCalls, 0, "no acknowledgement at machinery");
436
+ assert.equal(dispatched, 0, "and no session burned answering a reflex");
437
+ assert.equal(transcript.length, 0, "the peer seat hears nothing");
438
+ assert.ok(!assurance.openObligations().some((o) => o.key === item.raw_ref), "no debt opened for emitter-class inbound");
439
+ // The refusal is on the audit trail — same log family as the self-echo drops.
440
+ const { readFileSync } = await import("node:fs");
441
+ const drops = readFileSync(join(AGENT_DIR, "logs", "daemon", "dispatch-gate-drops.jsonl"), "utf-8")
442
+ .split("\n").filter(Boolean).map((l) => JSON.parse(l));
443
+ const drop = drops.find((d) => d.raw_ref === item.raw_ref);
444
+ assert.ok(drop, "the gate refusal is audited");
445
+ assert.equal(drop.reason, "peer-ack-shaped");
446
+ note("gate refusal audited to logs/daemon/dispatch-gate-drops.jsonl; item retired");
447
+
448
+ // The CONTRAST: the same shape from a human sender still gets the full path.
449
+ const humanItem = makeItem("HUMAN-TERSE-1", "On it — will do.");
450
+ let humanDispatched = 0;
451
+ await daemon.processItem(humanItem, "cohort", {
452
+ claudeAvailable: CLI_PRESENT,
453
+ classify: async () => COMPLEX,
454
+ isQuickReply: () => false,
455
+ sendHoldingMessage: ackSeam(async () => "Got it — I'll pick this up now."),
456
+ dispatch: () => { humanDispatched++; },
457
+ });
458
+ assert.equal(humanDispatched, 1, "a human's terse line is still worked on");
459
+ });
460
+
360
461
  // ===========================================================================
361
462
  // SCENARIO 7 — TWO ASKS, ONE ROOM. The owner sends a second thing before the
362
463
  // first is done. One session answers; the other exits 0 in silence.
@@ -420,3 +521,37 @@ test("SCENARIO 7 — a second ask in the same DM is not discharged by the first
420
521
  assert.match(rescue.text, /race between the poller's lock release/, "and B's actual work reaches the human");
421
522
  note("previously: B closed as `answered` on A's receipt — zero messages, ask gone");
422
523
  });
524
+
525
+ // ===========================================================================
526
+ // THE GATE'S OTHER SIDE — the fail-safe every scenario above suppresses.
527
+ // ===========================================================================
528
+
529
+ /**
530
+ * Mutation proof for `CLI_PRESENT`. Every scenario above declares the CLI
531
+ * present so that the ack path is exercised rather than skipped; this asserts
532
+ * the branch they are stepping over still behaves. Flip `CLI_PRESENT` to
533
+ * `() => false` and the scenarios go red; delete the `claudeAvailable` seam
534
+ * from THIS test and it goes red on any box with the CLI installed.
535
+ */
536
+ test("the ack is suppressed when the CLI is unavailable — but the debt is still opened", async () => {
537
+ const ask = "rebuild the index and tell me when it's done";
538
+ beginScenario("CLI UNAVAILABLE — silence with a tracked debt, never a promise", ask);
539
+ const item = makeItem("NO-CLI-1", ask);
540
+
541
+ let dispatched = 0;
542
+ await daemon.processItem(item, "cohort", {
543
+ claudeAvailable: () => false,
544
+ classify: async () => COMPLEX,
545
+ isQuickReply: () => false,
546
+ sendHoldingMessage: ackSeam(),
547
+ dispatch: () => { dispatched++; },
548
+ });
549
+
550
+ assert.equal(transcript.filter((m) => m.kind === "ack").length, 0,
551
+ "never promise a reply the CLI that would write it cannot start");
552
+ const rec = assurance.openObligations().find((o) => o.key === item.raw_ref);
553
+ assert.ok(rec, "the debt is opened anyway — silence is compensated by the sweep, not forgotten");
554
+ assert.equal(rec.state, "open");
555
+ assert.equal(dispatched, 1, "the work is still attempted; only the promise is withheld");
556
+ note("no ack, obligation open: the assurance sweep owns the compensation");
557
+ });