@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,242 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * sync-skill-packs.mjs — re-vendor a pinned upstream design skill pack.
4
+ *
5
+ * Usage:
6
+ * node scripts/vendor/sync-skill-packs.mjs <pack>[ <pack> …] re-fetch and rewrite
7
+ * node scripts/vendor/sync-skill-packs.mjs --all
8
+ * node scripts/vendor/sync-skill-packs.mjs --list
9
+ * node scripts/vendor/sync-skill-packs.mjs <pack> --dry-run report, write nothing
10
+ * node scripts/vendor/sync-skill-packs.mjs --all --dry-run RE-VERIFY every
11
+ * pack against its pinned upstream commit. This is the only thing that
12
+ * establishes provenance: `check-skill-packs.mjs` runs offline and can
13
+ * only prove the vendored trees have not moved since review, never that
14
+ * they came from upstream. Run this when auditing, not on every commit.
15
+ *
16
+ * What it does, for each pack in `scripts/vendor/skill-packs.mjs`:
17
+ * 1. downloads `https://codeload.github.com/<repo>/tar.gz/<sha>` — the PINNED
18
+ * commit, never a branch — into a temp dir and extracts it with `tar`;
19
+ * 2. copies ONLY the paths the pack's allow-list admits into
20
+ * `plugins/maestro-skills/vendor/<pack>/`, deleting anything already there
21
+ * that the allow-list no longer admits;
22
+ * 3. rewrites `UPSTREAM.json` with `{repo, sha, date, license, files:[{path,
23
+ * sha256}], omitted, notes}`, sorted, so the diff of a re-sync is readable.
24
+ *
25
+ * This is a MAINTAINER tool. It is the only place in the SDK that fetches skill
26
+ * content, it runs on a developer's machine before a commit, and nothing on a
27
+ * seat ever calls it: seats get the reviewed bytes from the npm tarball. It
28
+ * never executes anything out of the downloaded tree.
29
+ *
30
+ * Node builtins only (plus the system `tar`, which macOS and Linux both ship).
31
+ *
32
+ * @module scripts/vendor/sync-skill-packs
33
+ */
34
+
35
+ "use strict";
36
+
37
+ import { createHash } from "node:crypto";
38
+ import { execFileSync } from "node:child_process";
39
+ import {
40
+ mkdtempSync, mkdirSync, rmSync, readdirSync, readFileSync, writeFileSync, statSync, existsSync, copyFileSync,
41
+ } from "node:fs";
42
+ import { tmpdir } from "node:os";
43
+ import path from "node:path";
44
+ import { fileURLToPath } from "node:url";
45
+ import { PACKS, findPack, posix, isAllowedDest, manifestDigest } from "./skill-packs.mjs";
46
+
47
+ const REPO_ROOT = path.resolve(fileURLToPath(new URL(".", import.meta.url)), "..", "..");
48
+
49
+ /** Where vendored packs live, relative to the repo root. */
50
+ export const VENDOR_REL = path.join("plugins", "maestro-skills", "vendor");
51
+
52
+ /** @param {string} p @returns {string} */
53
+ export function sha256File(p) {
54
+ return createHash("sha256").update(readFileSync(p)).digest("hex");
55
+ }
56
+
57
+ /** Recursively list files under `dir`, as paths relative to it (POSIX). */
58
+ export function listFiles(dir, prefix = "") {
59
+ let out = [];
60
+ let entries;
61
+ try {
62
+ entries = readdirSync(dir, { withFileTypes: true });
63
+ } catch {
64
+ return out; // an absent directory is an empty one for our purposes
65
+ }
66
+ for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
67
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
68
+ if (e.isDirectory()) out = out.concat(listFiles(path.join(dir, e.name), rel));
69
+ else if (e.isFile()) out.push(rel);
70
+ }
71
+ return out;
72
+ }
73
+
74
+ /**
75
+ * Expand a pack's rules against a real upstream tree into {from, to} pairs.
76
+ * Pure over the injected lister so the planner is testable without a download.
77
+ *
78
+ * @param {import("./skill-packs.mjs").SkillPack} pack
79
+ * @param {string} srcRoot
80
+ * @param {{list?:(dir:string)=>string[], exists?:(p:string)=>boolean}} [deps]
81
+ * @returns {{ pairs: Array<{from:string,to:string}>, missing: string[] }}
82
+ */
83
+ export function planPack(pack, srcRoot, deps = {}) {
84
+ const list = deps.list || ((d) => listFiles(d));
85
+ const exists = deps.exists || existsSync;
86
+ const pairs = [];
87
+ const missing = [];
88
+ for (const rule of pack.rules) {
89
+ if (rule.type === "file") {
90
+ const abs = path.join(srcRoot, rule.from);
91
+ if (!exists(abs)) { missing.push(rule.from); continue; }
92
+ pairs.push({ from: rule.from, to: posix(rule.to) });
93
+ continue;
94
+ }
95
+ const absDir = path.join(srcRoot, rule.from);
96
+ if (!exists(absDir)) { missing.push(`${rule.from}/`); continue; }
97
+ let taken = 0;
98
+ for (const rel of list(absDir)) {
99
+ if (!rule.recursive && rel.includes("/")) continue;
100
+ const name = rel.slice(rel.lastIndexOf("/") + 1);
101
+ if (rule.match && !rule.match.test(name)) continue;
102
+ pairs.push({ from: `${rule.from}/${rel}`, to: posix(rule.to ? `${rule.to}/${rel}` : rel) });
103
+ taken += 1;
104
+ }
105
+ if (taken === 0) missing.push(`${rule.from}/ (matched nothing)`);
106
+ }
107
+ // A rule that produced a path the guard would reject is a registry bug, not a
108
+ // vendoring accident — say so loudly rather than writing an unverifiable tree.
109
+ for (const p of pairs) {
110
+ if (!isAllowedDest(pack, p.to)) throw new Error(`rule produced a path its own allow-list rejects: ${p.to}`);
111
+ }
112
+ pairs.sort((a, b) => (a.to < b.to ? -1 : a.to > b.to ? 1 : 0));
113
+ return { pairs, missing };
114
+ }
115
+
116
+ /** Download the pinned tarball and extract it; returns the extracted repo root. */
117
+ function fetchTree(pack, workDir) {
118
+ const url = `https://codeload.github.com/${pack.repo}/tar.gz/${pack.sha}`;
119
+ const tgz = path.join(workDir, "pack.tar.gz");
120
+ execFileSync("curl", ["-sSLf", "--max-time", "300", "-o", tgz, url], { stdio: ["ignore", "ignore", "pipe"] });
121
+ const outDir = path.join(workDir, "tree");
122
+ mkdirSync(outDir, { recursive: true });
123
+ execFileSync("tar", ["-xzf", tgz, "-C", outDir], { stdio: ["ignore", "ignore", "pipe"] });
124
+ const roots = readdirSync(outDir, { withFileTypes: true }).filter((e) => e.isDirectory());
125
+ if (roots.length !== 1) throw new Error(`unexpected tarball layout for ${pack.repo}: ${roots.length} top-level dirs`);
126
+ return path.join(outDir, roots[0].name);
127
+ }
128
+
129
+ /** Build the UPSTREAM.json body for a vendored pack. Pure. */
130
+ export function buildManifest(pack, files) {
131
+ return {
132
+ repo: pack.repo,
133
+ sha: pack.sha,
134
+ date: pack.date,
135
+ license: pack.license,
136
+ files: files.slice().sort((a, b) => (a.path < b.path ? -1 : 1)),
137
+ omitted: pack.omitted.slice(),
138
+ notes: pack.notes,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Re-vendor one pack. Returns a summary; throws only on a defect (no network,
144
+ * a moved upstream path, a rule that contradicts the allow-list).
145
+ * @param {string} id @param {{repoRoot?:string, dryRun?:boolean, log?:Function}} [o]
146
+ */
147
+ export function syncPack(id, o = {}) {
148
+ const pack = findPack(id);
149
+ if (!pack) throw new Error(`unknown pack "${id}" (known: ${PACKS.map((p) => p.id).join(", ")})`);
150
+ const repoRoot = o.repoRoot || REPO_ROOT;
151
+ const log = o.log || (() => {});
152
+ const destRoot = path.join(repoRoot, VENDOR_REL, pack.id);
153
+ const work = mkdtempSync(path.join(tmpdir(), `maestro-vendor-${pack.id}-`));
154
+ try {
155
+ const srcRoot = fetchTree(pack, work);
156
+ const { pairs, missing } = planPack(pack, srcRoot);
157
+ if (missing.length) throw new Error(`${pack.id}: upstream paths absent at ${pack.sha}: ${missing.join(", ")}`);
158
+ const wanted = new Set(pairs.map((p) => p.to));
159
+ const existingFiles = listFiles(destRoot);
160
+ const removed = existingFiles.filter((f) => f !== "UPSTREAM.json" && !wanted.has(f));
161
+ if (!o.dryRun) {
162
+ for (const rel of removed) rmSync(path.join(destRoot, rel), { force: true });
163
+ for (const p of pairs) {
164
+ const dest = path.join(destRoot, p.to);
165
+ mkdirSync(path.dirname(dest), { recursive: true });
166
+ copyFileSync(path.join(srcRoot, p.from), dest);
167
+ // Vendored content is data. Strip any executable bit the upstream tree
168
+ // carried unless the pack explicitly allows one there.
169
+ const mode = statSync(dest).mode & 0o777;
170
+ const want = 0o644;
171
+ if (mode !== want) execFileSync("chmod", ["644", dest]);
172
+ }
173
+ pruneEmptyDirs(destRoot);
174
+ }
175
+ const files = pairs.map((p) => ({
176
+ path: p.to,
177
+ sha256: o.dryRun ? sha256File(path.join(srcRoot, p.from)) : sha256File(path.join(destRoot, p.to)),
178
+ }));
179
+ const manifest = buildManifest(pack, files);
180
+ const manifestPath = path.join(destRoot, "UPSTREAM.json");
181
+ if (!o.dryRun) {
182
+ mkdirSync(destRoot, { recursive: true });
183
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
184
+ }
185
+ // The content pin. `UPSTREAM.json` is written from the bytes it records, so
186
+ // it can only ever agree with itself; the digest below lives in
187
+ // skill-packs.mjs, a reviewed source file no vendored tree can reach, and is
188
+ // what makes a change to the vendored file list show up in a diff as one
189
+ // line rather than as a data blob nobody reads. A re-sync that shifts it is
190
+ // a real upstream change and wants a real look — hence the loud line.
191
+ const contentSha256 = manifestDigest(files);
192
+ const pinned = pack.contentSha256 || null;
193
+ if (pinned && pinned !== contentSha256) {
194
+ log(`${pack.id}: CONTENT CHANGED — review the diff, then set contentSha256: "${contentSha256}" in scripts/vendor/skill-packs.mjs (was ${pinned})`);
195
+ } else if (!pinned) {
196
+ log(`${pack.id}: set contentSha256: "${contentSha256}" in scripts/vendor/skill-packs.mjs`);
197
+ }
198
+ log(`${pack.id}: ${files.length} file(s) from ${pack.repo}@${pack.sha.slice(0, 12)}${removed.length ? `, ${removed.length} removed` : ""}${o.dryRun ? " (dry-run)" : ""}`);
199
+ return { pack: pack.id, files, removed, contentSha256, pinned, dryRun: !!o.dryRun };
200
+ } finally {
201
+ rmSync(work, { recursive: true, force: true });
202
+ }
203
+ }
204
+
205
+ /** Remove directories left empty after a prune. */
206
+ function pruneEmptyDirs(root) {
207
+ let entries;
208
+ try { entries = readdirSync(root, { withFileTypes: true }); } catch { return; }
209
+ for (const e of entries) {
210
+ if (!e.isDirectory()) continue;
211
+ const child = path.join(root, e.name);
212
+ pruneEmptyDirs(child);
213
+ try { if (readdirSync(child).length === 0) rmSync(child, { recursive: true, force: true }); } catch { /* a racing reader is not our problem */ }
214
+ }
215
+ }
216
+
217
+ async function main(argv) {
218
+ const args = argv.slice(2);
219
+ if (args.includes("--list") || args.length === 0) {
220
+ for (const p of PACKS) console.log(`${p.id.padEnd(14)} ${p.repo}@${p.sha.slice(0, 12)} ${p.license}`);
221
+ if (args.length === 0) console.log("\nusage: node scripts/vendor/sync-skill-packs.mjs <pack>|--all [--dry-run]");
222
+ return 0;
223
+ }
224
+ const dryRun = args.includes("--dry-run");
225
+ const ids = args.includes("--all") ? PACKS.map((p) => p.id) : args.filter((a) => !a.startsWith("--"));
226
+ let code = 0;
227
+ for (const id of ids) {
228
+ try {
229
+ syncPack(id, { dryRun, log: (m) => console.log(`sync-skill-packs: ${m}`) });
230
+ } catch (err) {
231
+ console.error(`sync-skill-packs: ${id} FAILED — ${err && err.message ? err.message : err}`);
232
+ code = 1;
233
+ }
234
+ }
235
+ return code;
236
+ }
237
+
238
+ if (import.meta.url === `file://${process.argv[1]}`) {
239
+ main(process.argv).then((c) => process.exit(c)).catch((e) => { console.error("sync-skill-packs: ERROR", e && e.message ? e.message : e); process.exit(2); });
240
+ }
241
+
242
+ export default { syncPack, planPack, buildManifest, listFiles, sha256File, VENDOR_REL };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Tests for the vendoring maintainer tool — the half that can run with no
3
+ * network. `syncPack` itself downloads a pinned tarball and is not exercised
4
+ * here; what IS exercised is the planner and the content pin, because those are
5
+ * the two places a re-sync can quietly change what the fleet reads.
6
+ *
7
+ * @module scripts/vendor/sync-skill-packs.test
8
+ */
9
+
10
+ import { describe, it } from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import path from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { planPack, buildManifest, sha256File } from "./sync-skill-packs.mjs";
17
+ import { PACKS, findPack, manifestDigest } from "./skill-packs.mjs";
18
+
19
+ const REPO_ROOT = path.resolve(fileURLToPath(new URL(".", import.meta.url)), "..", "..");
20
+ const VENDOR = path.join(REPO_ROOT, "plugins", "maestro-skills", "vendor");
21
+
22
+ describe("sync-skill-packs: the content pin", () => {
23
+ it("manifestDigest is order-independent and moves for any file added, removed or changed", () => {
24
+ const a = [{ path: "SKILL.md", sha256: "aa" }, { path: "reference/x.md", sha256: "bb" }];
25
+ assert.equal(manifestDigest(a), manifestDigest([...a].reverse()), "sorted, so a re-sort is not a change");
26
+ assert.notEqual(manifestDigest(a), manifestDigest([...a, { path: "reference/y.md", sha256: "cc" }]));
27
+ assert.notEqual(manifestDigest(a), manifestDigest([a[0]]));
28
+ assert.notEqual(manifestDigest(a), manifestDigest([a[0], { path: "reference/x.md", sha256: "b0" }]));
29
+ });
30
+
31
+ it("what the syncer would write for each committed pack still matches its pinned digest", () => {
32
+ // buildManifest is what a re-sync produces; if it and the committed
33
+ // UPSTREAM.json disagree, someone hand-edited a vendored tree.
34
+ for (const pack of PACKS) {
35
+ const doc = JSON.parse(readFileSync(path.join(VENDOR, pack.id, "UPSTREAM.json"), "utf8"));
36
+ const rebuilt = buildManifest(pack, doc.files);
37
+ assert.deepEqual(rebuilt.files, doc.files, pack.id);
38
+ assert.deepEqual(rebuilt.omitted, doc.omitted, `${pack.id} omitted[] matches the registry`);
39
+ assert.equal(rebuilt.notes, doc.notes, `${pack.id} notes match the registry`);
40
+ assert.equal(manifestDigest(rebuilt.files), pack.contentSha256, pack.id);
41
+ }
42
+ });
43
+
44
+ it("every recorded sha256 is the sha256 of the file on disk", () => {
45
+ for (const pack of PACKS) {
46
+ const doc = JSON.parse(readFileSync(path.join(VENDOR, pack.id, "UPSTREAM.json"), "utf8"));
47
+ for (const f of doc.files) {
48
+ assert.equal(sha256File(path.join(VENDOR, pack.id, f.path)), f.sha256, `${pack.id}/${f.path}`);
49
+ }
50
+ }
51
+ });
52
+ });
53
+
54
+ describe("sync-skill-packs: the planner", () => {
55
+ /** A fake upstream tree, so the planner runs with no network. */
56
+ function upstream(files) {
57
+ const root = mkdtempSync(path.join(tmpdir(), "maestro-upstream-"));
58
+ for (const [rel, body] of Object.entries(files)) {
59
+ const abs = path.join(root, rel);
60
+ mkdirSync(path.dirname(abs), { recursive: true });
61
+ writeFileSync(abs, body);
62
+ }
63
+ return { root, cleanup: () => rmSync(root, { recursive: true, force: true }) };
64
+ }
65
+
66
+ it("unlazy's rules take the gate checker and refuse the hook machinery", () => {
67
+ // The pack is vendored for its gate checker. `install-hooks.mjs` writes a
68
+ // Stop hook into a settings file — `--global` into the one every session on
69
+ // the machine reads — so it is excluded at the rule, not discouraged in
70
+ // prose: a file that reaches the seat can be run whatever its mode says.
71
+ const t = upstream({
72
+ "SKILL.md": "---\nname: unlazy\n---\n",
73
+ "SECURITY.md": "#\n",
74
+ "LICENSE": "MIT\n",
75
+ "references/gates.md": "#\n",
76
+ "templates/PLAN.md": "#\n",
77
+ "scripts/gate-check.mjs": "//\n",
78
+ "scripts/lib/gates.mjs": "//\n",
79
+ "scripts/install-hooks.mjs": "// writes a Stop hook\n",
80
+ "scripts/stop-hook.mjs": "// the hook body\n",
81
+ });
82
+ try {
83
+ const { pairs, missing } = planPack(findPack("unlazy"), t.root);
84
+ const took = pairs.map((p) => p.to);
85
+ assert.deepEqual(missing, []);
86
+ assert.ok(took.includes("scripts/gate-check.mjs"));
87
+ assert.ok(took.includes("scripts/lib/gates.mjs"));
88
+ assert.equal(took.includes("scripts/install-hooks.mjs"), false);
89
+ assert.equal(took.includes("scripts/stop-hook.mjs"), false);
90
+ } finally { t.cleanup(); }
91
+ });
92
+
93
+ it("a rule that produced a path its own allow-list rejects is a defect, not a quiet write", () => {
94
+ const pack = {
95
+ ...findPack("unlazy"),
96
+ rules: [{ type: "dir", from: "scripts", to: "../escape", match: /\.mjs$/, recursive: true }],
97
+ };
98
+ const t = upstream({ "scripts/x.mjs": "//\n" });
99
+ try {
100
+ assert.throws(() => planPack(pack, t.root), /allow-list rejects/);
101
+ } finally { t.cleanup(); }
102
+ });
103
+ });
@@ -16,7 +16,10 @@
16
16
  # auto-expiring ceiling = liveCount-2 that the resource governor
17
17
  # reads to admit fewer sessions. NO kills.
18
18
  # HARD (sys 80–90% / claude ≥75%) → SIGTERM the youngest/biggest
19
- # subagents, sparing the oldest "main" session (MIN_AGE_SECONDS).
19
+ # subagents, sparing the oldest "main" session (MIN_AGE_SECONDS)
20
+ # and — audit F16 — NEVER the seat's interactive main session
21
+ # (`--name <first>-main`) or its feed (scripts/session/feed.mjs),
22
+ # whatever their age or size. See is_protected_cmdline.
20
23
  # CRITICAL (sys ≥90%, sustained 2 consecutive runs) → clean daemon RESTART
21
24
  # via `launchctl kickstart -k` (the daemon re-reads state/queues +
22
25
  # cooldowns + sweeps stale claims). NOT a halt. Only after 3 failed
@@ -156,6 +159,34 @@ count_claude_processes() {
156
159
 
157
160
  # --- Identification ----------------------------------------------------------
158
161
 
162
+ # Audit F16. The oldest-process heuristic below is not enough: after a relaunch
163
+ # the seat's interactive main session is the YOUNGEST claude process, and the
164
+ # HARD tier killed it. The main session is identified by its command line, not
165
+ # its age: `claude --name <first>-main …` (the supervisor launches it so) and
166
+ # the feed running inside it (`node scripts/session/feed.mjs`). Pure: takes the
167
+ # command line, exits 0 when the process must never be signalled.
168
+ is_protected_cmdline() {
169
+ local cmdline="${1:-}"
170
+ [ -z "$cmdline" ] && return 1
171
+ # `--name <token>-main` as a WHOLE token (not -mainline, not -main-2).
172
+ if printf '%s\n' "$cmdline" | grep -Eq -- '(^|[[:space:]])--name[[:space:]]+[A-Za-z0-9_.-]+-main([[:space:]]|$)'; then
173
+ return 0
174
+ fi
175
+ case "$cmdline" in
176
+ *scripts/session/feed.mjs*) return 0 ;;
177
+ esac
178
+ return 1
179
+ }
180
+
181
+ # Full command line of a pid ("" when the process is gone). `-ww` = unlimited
182
+ # width: without it macOS ps clips `command=` to the terminal width when
183
+ # stdout is a tty (an operator's --check / --dry-run), and a long claude argv
184
+ # can lose its `--name <first>-main` token — which would un-protect the main
185
+ # session exactly when a human is watching.
186
+ cmdline_of() {
187
+ /bin/ps -ww -o command= -p "$1" 2>/dev/null || true
188
+ }
189
+
159
190
  identify_main_session() {
160
191
  # The oldest claude process is treated as the "main" session and spared by
161
192
  # HARD-tier kills. (longest-running heuristic)
@@ -201,6 +232,11 @@ kill_youngest_biggest_subagents() {
201
232
  log_event "info" "skip_young" "PID $pid age ${age}s < ${MIN_AGE_SECONDS}s minimum"
202
233
  continue
203
234
  fi
235
+ # Audit F16: the interactive main session and its feed are never candidates.
236
+ if is_protected_cmdline "$(cmdline_of "$pid")"; then
237
+ log_event "info" "skip_protected" "PID $pid is the main session / feed — never signalled"
238
+ continue
239
+ fi
204
240
  if [ "$killed" -ge "$target_count" ]; then
205
241
  break
206
242
  fi
@@ -0,0 +1,64 @@
1
+ /**
2
+ * memory-watchdog.test.mjs — audit F16: the HARD tier could SIGTERM the
3
+ * seat's long-lived interactive main session (it only spared the OLDEST
4
+ * claude process, and a freshly relaunched main session is not the oldest).
5
+ *
6
+ * The rule under test is a pure shell function, `is_protected_cmdline`,
7
+ * extracted from the script and run under bash: a command line carrying
8
+ * `--name <anything>-main` or `scripts/session/feed.mjs` is never a kill
9
+ * candidate. The kill loop is also checked textually for the call site, so
10
+ * the function cannot be defined and then not used.
11
+ */
12
+
13
+ import { test } from "node:test";
14
+ import assert from "node:assert/strict";
15
+ import { readFileSync } from "node:fs";
16
+ import { spawnSync } from "node:child_process";
17
+ import { join, dirname } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+
20
+ const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "memory-watchdog.sh");
21
+
22
+ /** Extract `is_protected_cmdline` from the script and evaluate it on one command line. Exit 0 = protected. */
23
+ function isProtected(cmdline) {
24
+ const src = readFileSync(SCRIPT, "utf8");
25
+ const m = src.match(/\nis_protected_cmdline\(\) \{[\s\S]*?\n\}\n/);
26
+ assert.ok(m, "memory-watchdog.sh defines is_protected_cmdline()");
27
+ const r = spawnSync("/bin/bash", ["-c", `${m[0]}\nis_protected_cmdline "$1"`, "_", cmdline], { encoding: "utf8" });
28
+ return r.status === 0;
29
+ }
30
+
31
+ test("the main session (`--name <first>-main`) is protected, whatever the first name", () => {
32
+ assert.equal(isProtected("claude --name alex-main --resume 1234 --dangerously-skip-permissions"), true);
33
+ assert.equal(isProtected("/opt/homebrew/bin/claude --dangerously-skip-permissions --name marc-main"), true);
34
+ assert.equal(isProtected("node /Users/x/.npm-global/lib/node_modules/@anthropic-ai/claude-code/cli.js --name olivia-main"), true);
35
+ });
36
+
37
+ test("the session feed (scripts/session/feed.mjs) is protected", () => {
38
+ assert.equal(isProtected("node /Users/alex/alex-ai/scripts/session/feed.mjs"), true);
39
+ assert.equal(isProtected("/usr/local/bin/node scripts/session/feed.mjs --agent-root /x"), true);
40
+ });
41
+
42
+ test("sub-sessions, peers and print spawns are NOT protected", () => {
43
+ assert.equal(isProtected("claude --print --output-format json --dangerously-skip-permissions prompt"), false);
44
+ assert.equal(isProtected("claude --name alex-research --dangerously-skip-permissions"), false);
45
+ assert.equal(isProtected("claude --name alex-mainline"), false, "-main must be a whole token");
46
+ assert.equal(isProtected("claude --name alex-main-2"), false);
47
+ assert.equal(isProtected("node scripts/daemon/maestro-daemon.mjs"), false);
48
+ assert.equal(isProtected(""), false);
49
+ });
50
+
51
+ test("the HARD-tier kill loop consults is_protected_cmdline before SIGTERM and logs the skip", () => {
52
+ const src = readFileSync(SCRIPT, "utf8");
53
+ const loop = src.slice(src.indexOf("kill_youngest_biggest_subagents() {"), src.indexOf('done <<< "$(get_claude_processes)"'));
54
+ assert.match(loop, /is_protected_cmdline/, "the kill loop calls the guard");
55
+ assert.match(loop, /skip_protected/, "a protected skip is logged");
56
+ assert.ok(loop.indexOf("is_protected_cmdline") < loop.indexOf('kill -TERM "$pid"'), "the guard runs before the signal");
57
+ });
58
+
59
+ test("cmdline_of reads the FULL command line (ps -ww) so a long argv is never cut before --name <first>-main on a narrow tty", () => {
60
+ const src = readFileSync(SCRIPT, "utf8");
61
+ const m = src.match(/\ncmdline_of\(\) \{[\s\S]*?\n\}\n/);
62
+ assert.ok(m, "memory-watchdog.sh defines cmdline_of()");
63
+ assert.match(m[0], /\/bin\/ps -ww /, "ps must be given -ww (unlimited width) — macOS truncates `-o command=` to the terminal width otherwise");
64
+ });
@@ -1,94 +0,0 @@
1
- #!/bin/bash
2
- # =============================================================================
3
- # boot-claude-session.sh — Launch interactive Claude Code session on boot
4
- # =============================================================================
5
- #
6
- # Opens Terminal.app with a Claude Code interactive session in the agent's
7
- # working directory. Called via launchd at login.
8
- #
9
- # This is separate from the maestro daemon (which runs headless as a Node.js
10
- # process for polling/dispatching). This script provides a visible, interactive
11
- # Claude Code session that the operator can observe and interact with.
12
- #
13
- # Usage:
14
- # ./scripts/setup/boot-claude-session.sh <agent-dir>
15
- #
16
- # =============================================================================
17
-
18
- set -euo pipefail
19
-
20
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
- MAESTRO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
22
-
23
- # Agent directory required as first argument (e.g. ~/<firstname>-ai).
24
- AGENT_DIR="${1:?Agent directory required as first argument (e.g. ~/<firstname>-ai)}"
25
- if [ ! -d "$AGENT_DIR" ]; then
26
- echo "ERROR: Agent directory does not exist: $AGENT_DIR" >&2
27
- exit 1
28
- fi
29
-
30
- AGENT_NAME=$(basename "$AGENT_DIR")
31
- # Resolve the agent's display name from config/agent.json (SOT), falling
32
- # back to the directory basename. Used for the Terminal window banner.
33
- AGENT_DISPLAY="$AGENT_NAME"
34
- if [ -f "$AGENT_DIR/config/agent.json" ]; then
35
- if command -v jq >/dev/null 2>&1; then
36
- fn=$(jq -r '.fullName // empty' "$AGENT_DIR/config/agent.json")
37
- [ -n "$fn" ] && AGENT_DISPLAY="$fn"
38
- fi
39
- fi
40
- LOG_FILE="$MAESTRO_DIR/logs/watchdog/$(date +%Y-%m-%d)-boot-session.log"
41
- mkdir -p "$(dirname "$LOG_FILE")"
42
-
43
- log() {
44
- echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] $1" | tee -a "$LOG_FILE"
45
- }
46
-
47
- # Wait for the system to settle after login (network, services, etc.)
48
- log "Waiting 15s for system to settle..."
49
- sleep 15
50
-
51
- # Check for emergency stop
52
- if [ -f "$AGENT_DIR/.emergency-stop" ]; then
53
- log "Emergency stop active — not starting Claude session"
54
- exit 0
55
- fi
56
-
57
- if [ -f "$MAESTRO_DIR/.emergency-stop" ]; then
58
- log "Emergency stop active in maestro — not starting Claude session"
59
- exit 0
60
- fi
61
-
62
- # Check if claude CLI is available
63
- if ! command -v claude &>/dev/null; then
64
- # Try common paths
65
- CLAUDE_PATH=""
66
- for p in "$HOME/.local/bin/claude" "/opt/homebrew/bin/claude" "/usr/local/bin/claude"; do
67
- if [ -x "$p" ]; then
68
- CLAUDE_PATH="$p"
69
- break
70
- fi
71
- done
72
-
73
- if [ -z "$CLAUDE_PATH" ]; then
74
- log "ERROR: claude CLI not found"
75
- exit 1
76
- fi
77
- else
78
- CLAUDE_PATH=$(which claude)
79
- fi
80
-
81
- log "Starting Claude Code session in $AGENT_DIR (claude: $CLAUDE_PATH)"
82
-
83
- # Open Terminal.app with Claude Code running in the agent directory
84
- osascript <<APPLESCRIPT
85
- tell application "Terminal"
86
- activate
87
- -- Open a new window with claude running in the agent directory
88
- do script "cd '$AGENT_DIR' && clear && echo '╔══════════════════════════════════════════════════════════╗' && echo '║ $AGENT_DISPLAY — Boot Session ($(date +%Y-%m-%d))' && echo '╠══════════════════════════════════════════════════════════╣' && echo '║ Agent dir: $AGENT_DIR' && echo '║ Claude: $CLAUDE_PATH' && echo '╚══════════════════════════════════════════════════════════╝' && echo '' && '$CLAUDE_PATH'"
89
- -- Set the window title
90
- set custom title of front window to "$AGENT_NAME — Claude Code"
91
- end tell
92
- APPLESCRIPT
93
-
94
- log "Terminal window opened with Claude Code session"