@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.
- package/.env.example +37 -22
- package/README.md +2 -0
- package/bin/maestro.mjs +117 -39
- package/bin/maestro.test.mjs +175 -5
- package/docs/guides/front-door-session.md +313 -0
- package/docs/guides/mac-mini.md +100 -28
- package/docs/guides/org-onboarding.md +1 -1
- package/docs/guides/setup-wizard.md +9 -5
- package/docs/runbooks/cohort-cutover.md +11 -1
- package/docs/runbooks/mac-mini-bootstrap.md +38 -63
- package/lib/cadence-bus-requeue.test.mjs +83 -0
- package/lib/cadence-bus.mjs +43 -7
- package/lib/channels/inbox-item.mjs +59 -2
- package/lib/cli/board.mjs +285 -0
- package/lib/cli/board.test.mjs +227 -0
- package/lib/cli/design.mjs +185 -0
- package/lib/cli/design.test.mjs +270 -0
- package/lib/cli/doctor-checks.mjs +441 -0
- package/lib/cli/doctor-checks.test.mjs +336 -0
- package/lib/cli/global-setup-extras.mjs +454 -0
- package/lib/cli/global-setup-extras.test.mjs +462 -0
- package/lib/cli/inbox.mjs +304 -0
- package/lib/cli/inbox.test.mjs +230 -0
- package/lib/cli/session-ack.mjs +63 -0
- package/lib/cli/session-ack.test.mjs +63 -0
- package/lib/cli/session.mjs +760 -0
- package/lib/cli/session.test.mjs +613 -0
- package/lib/collective/global-config.mjs +209 -6
- package/lib/collective/global-config.test.mjs +145 -0
- package/lib/collective/global-skills.mjs +145 -0
- package/lib/collective/global-skills.test.mjs +126 -0
- package/lib/collective/presence.mjs +4 -3
- package/lib/collective/vendor-skills.mjs +305 -0
- package/lib/collective/vendor-skills.test.mjs +306 -0
- package/lib/comms/send-gate.mjs +115 -0
- package/lib/comms/send-gate.test.mjs +113 -0
- package/lib/design/design-md.mjs +793 -0
- package/lib/design/design-md.test.mjs +318 -0
- package/lib/design/fixtures/DESIGN.golden.md +238 -0
- package/lib/design/fixtures/PRODUCT.golden.md +67 -0
- package/lib/design/fixtures/foundation.json +133 -0
- package/lib/design/refresh-gate.mjs +154 -0
- package/lib/design/refresh-gate.test.mjs +144 -0
- package/lib/design/write.mjs +275 -0
- package/lib/design/write.test.mjs +241 -0
- package/lib/feature-init.mjs +2 -2
- package/lib/mcp/server.test.mjs +9 -4
- package/lib/model-router/spawn.test.mjs +21 -0
- package/lib/org/board-mine-cache.mjs +99 -0
- package/lib/org/board-mine-cache.test.mjs +53 -0
- package/lib/org/board.mjs +11 -0
- package/lib/org/board.test.mjs +11 -1
- package/lib/org/client.mjs +36 -0
- package/lib/org/client.test.mjs +46 -0
- package/lib/org/inbound/directedness.mjs +18 -2
- package/lib/org/inbound/directedness.test.mjs +58 -0
- package/lib/org/inbound/index.mjs +8 -1
- package/lib/org/inbound/index.test.mjs +22 -0
- package/lib/org/mesh-directives.test.mjs +110 -0
- package/lib/org/mesh.mjs +61 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +52 -0
- package/lib/org/protocol.test.mjs +12 -1
- package/lib/org/registry.mjs +3 -2
- package/lib/org/tool-surface.mjs +120 -0
- package/lib/org/tool-surface.test.mjs +118 -5
- package/lib/prompts/parallelism.mjs +79 -0
- package/lib/prompts/parallelism.test.mjs +177 -0
- package/lib/security/external-content.mjs +1 -1
- package/lib/security/external-content.test.mjs +17 -0
- package/lib/session/config.mjs +137 -0
- package/lib/session/config.test.mjs +92 -0
- package/lib/session/feed-core.mjs +229 -0
- package/lib/session/feed-core.test.mjs +198 -0
- package/lib/session/first-run.mjs +126 -0
- package/lib/session/first-run.test.mjs +121 -0
- package/lib/session/frontdoor.mjs +266 -0
- package/lib/session/frontdoor.test.mjs +205 -0
- package/lib/session/handoffs.mjs +295 -0
- package/lib/session/handoffs.test.mjs +183 -0
- package/lib/session/identity.mjs +220 -0
- package/lib/session/identity.test.mjs +180 -0
- package/lib/session/inbox-claims.mjs +434 -0
- package/lib/session/inbox-claims.test.mjs +286 -0
- package/lib/session/launch-args.mjs +161 -0
- package/lib/session/launch-args.test.mjs +157 -0
- package/lib/session/liveness.mjs +174 -0
- package/lib/session/liveness.test.mjs +100 -0
- package/lib/session/status-summary.mjs +172 -0
- package/lib/session/status-summary.test.mjs +118 -0
- package/lib/session-permissions.mjs +39 -3
- package/lib/session-permissions.test.mjs +20 -0
- package/lib/setup/claude-probe.mjs +161 -24
- package/lib/setup/claude-probe.test.mjs +187 -0
- package/lib/setup/sections/learning.mjs +2 -1
- package/lib/setup/sections/model.mjs +104 -24
- package/lib/setup/sections/model.test.mjs +240 -0
- package/lib/setup/sections/org.mjs +27 -2
- package/lib/setup/sections/org.test.mjs +35 -2
- package/lib/setup/sections/verify.mjs +5 -0
- package/lib/setup/state.mjs +30 -10
- package/lib/setup/state.test.mjs +24 -1
- package/lib/singleton.js +11 -3
- package/lib/singleton.test.mjs +16 -0
- package/lib/subagents/lock.mjs +1 -1
- package/lib/telemetry/collect.mjs +270 -6
- package/lib/telemetry/collect.test.mjs +196 -1
- package/lib/upgrade/global-refresh.mjs +108 -0
- package/lib/upgrade/global-refresh.test.mjs +65 -0
- package/lib/upgrade/launchd-reconcile.mjs +327 -0
- package/lib/upgrade/launchd-reconcile.test.mjs +272 -0
- package/lib/upgrade/post-steps.mjs +151 -0
- package/lib/upgrade/post-steps.test.mjs +200 -0
- package/lib/upgrade/verify.mjs +215 -0
- package/lib/upgrade/verify.test.mjs +164 -0
- package/lib/voice/outbound.mjs +3 -2
- package/lib/voice/post-call-brief.mjs +2 -1
- package/lib/voice/session-rotation.mjs +6 -1
- package/lib/voice/session-rotation.test.mjs +114 -0
- package/package.json +3 -3
- package/plugins/maestro-skills/plugin.json +25 -1
- package/plugins/maestro-skills/skills/board-work.md +63 -0
- package/plugins/maestro-skills/skills/cohort-design.md +153 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +80 -0
- package/plugins/maestro-skills/skills/main-session.md +102 -0
- package/plugins/maestro-skills/skills/peer-sessions.md +65 -0
- package/plugins/maestro-skills/skills/persona-discipline.md +75 -0
- package/plugins/maestro-skills/vendor/emilkowalski/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/emilkowalski/UPSTREAM.json +70 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/RECIPES.md +324 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animate/SKILL.md +199 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/animation-vocabulary/SKILL.md +173 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/apple-design/SKILL.md +282 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/emil-design-eng/SKILL.md +674 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/find-animation-opportunities/SKILL.md +132 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/AUDIT.md +115 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/PLAN-TEMPLATE.md +73 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/improve-animations/SKILL.md +101 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/PICKER.md +197 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/prototype/SKILL.md +90 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/SKILL.md +112 -0
- package/plugins/maestro-skills/vendor/emilkowalski/skills/review-animations/STANDARDS.md +187 -0
- package/plugins/maestro-skills/vendor/impeccable/LICENSE +191 -0
- package/plugins/maestro-skills/vendor/impeccable/NOTICE.md +11 -0
- package/plugins/maestro-skills/vendor/impeccable/SKILL.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/UPSTREAM.json +201 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-asset-producer.md +42 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-documenter.md +29 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-finish-reviewer.md +43 -0
- package/plugins/maestro-skills/vendor/impeccable/agents/impeccable-manual-edit-applier.md +97 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.md +312 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/adapt.native.md +58 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/android.md +46 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/animate.md +89 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.md +136 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/audit.native.md +139 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/bolder.md +33 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/clarify.md +94 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/colorize.md +86 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft-floor.md +44 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/craft.md +5 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/critique.md +806 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/asset-producer.md +37 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/documenter.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/finish-reviewer.md +38 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/degraded/manual-edit-applier.md +92 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/delight.md +70 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/distill.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/doctor.md +54 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/document.md +416 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/extract.md +69 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/harden.md +336 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/hooks.md +111 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/init.md +131 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/ios.md +51 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/layout.md +84 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live-setup.md +104 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/live.md +325 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/new-work.md +147 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/onboard.md +234 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/operate.md +61 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/optimize.md +258 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/overdrive.md +127 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/polish.md +105 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/quieter.md +99 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/routing.md +24 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/shape.md +59 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/typeset.md +80 -0
- package/plugins/maestro-skills/vendor/impeccable/reference/visualize.md +46 -0
- package/plugins/maestro-skills/vendor/taste-skill/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/taste-skill/UPSTREAM.json +37 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/minimalist-skill/SKILL.md +85 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/redesign-skill/SKILL.md +178 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/soft-skill/SKILL.md +98 -0
- package/plugins/maestro-skills/vendor/taste-skill/skills/taste-skill/SKILL.md +1206 -0
- package/plugins/maestro-skills/vendor/unlazy/LICENSE +21 -0
- package/plugins/maestro-skills/vendor/unlazy/SECURITY.md +72 -0
- package/plugins/maestro-skills/vendor/unlazy/SKILL.md +104 -0
- package/plugins/maestro-skills/vendor/unlazy/UPSTREAM.json +94 -0
- package/plugins/maestro-skills/vendor/unlazy/references/dispatch.md +82 -0
- package/plugins/maestro-skills/vendor/unlazy/references/gates.md +149 -0
- package/plugins/maestro-skills/vendor/unlazy/references/method.md +49 -0
- package/plugins/maestro-skills/vendor/unlazy/references/orchestration.md +107 -0
- package/plugins/maestro-skills/vendor/unlazy/references/parallel.md +133 -0
- package/plugins/maestro-skills/vendor/unlazy/references/token-economy.md +48 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/dispatch-check.mjs +139 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-check.mjs +960 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/gate-lint.mjs +245 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/check-supervisor.mjs +46 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/dispatch.mjs +293 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/gates.mjs +953 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/process-tree.mjs +161 -0
- package/plugins/maestro-skills/vendor/unlazy/scripts/lib/regex-worker.mjs +9 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/PLAN.md +116 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-leaf.md +51 -0
- package/plugins/maestro-skills/vendor/unlazy/templates/gates-node.md +51 -0
- package/scaffold/CLAUDE.md +24 -0
- package/scripts/ci/check-durable-write-seam.mjs +147 -0
- package/scripts/ci/check-durable-write-seam.test.mjs +90 -0
- package/scripts/ci/check-skill-packs.mjs +388 -0
- package/scripts/ci/check-skill-packs.test.mjs +495 -0
- package/scripts/ci/check.mjs +6 -0
- package/scripts/collective/hook-runner.mjs +39 -4
- package/scripts/collective/hook-runner.test.mjs +85 -2
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +96 -0
- package/scripts/daemon/agent-daemon-design.test.mjs +238 -0
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +60 -0
- package/scripts/daemon/agent-daemon.mjs +249 -10
- package/scripts/daemon/agent-daemon.test.mjs +73 -0
- package/scripts/daemon/assurance-e2e.test.mjs +141 -6
- package/scripts/daemon/assurance.mjs +461 -37
- package/scripts/daemon/assurance.test.mjs +408 -43
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +393 -0
- package/scripts/daemon/cadence-consumer.mjs +289 -89
- package/scripts/daemon/cadence-handlers.mjs +53 -0
- package/scripts/daemon/classifier.mjs +1 -1
- package/scripts/daemon/dispatcher-resume.test.mjs +166 -0
- package/scripts/daemon/dispatcher.mjs +127 -19
- package/scripts/daemon/health.mjs +12 -1
- package/scripts/daemon/inbox-deferral-session.test.mjs +49 -0
- package/scripts/daemon/inbox-deferral.mjs +6 -0
- package/scripts/daemon/lib/self-echo.mjs +201 -0
- package/scripts/daemon/lib/self-echo.test.mjs +153 -0
- package/scripts/daemon/maestro-daemon.mjs +3 -0
- package/scripts/daemon/prompt-builder.mjs +19 -3
- package/scripts/daemon/responder.mjs +51 -40
- package/scripts/daemon/sdk-version.mjs +51 -0
- package/scripts/daemon/sdk-version.test.mjs +31 -0
- package/scripts/hooks/pre-send-audit.sh +97 -4
- package/scripts/hooks/pre-send-audit.test.mjs +140 -1
- package/scripts/local-triggers/autoupdate.sh +243 -19
- package/scripts/local-triggers/autoupdate.test.mjs +518 -0
- package/scripts/local-triggers/generate-plists.sh +24 -1
- package/scripts/local-triggers/generate-plists.test.mjs +49 -11
- package/scripts/org/send-orgmail.first-contact.test.mjs +102 -0
- package/scripts/org/send-orgmail.mjs +27 -3
- package/scripts/poller/inbox-privilege-injection.test.mjs +167 -0
- package/scripts/poller/slack-poller.mjs +13 -1
- package/scripts/poller/utils.mjs +46 -1
- package/scripts/poller-launchd/install.sh +19 -11
- package/scripts/poller-launchd/install.test.mjs +243 -0
- package/scripts/poller-launchd/launchd-poller-wrapper.sh +92 -0
- package/scripts/poller-launchd/migrate.sh +66 -0
- package/scripts/poller-launchd/poller.plist.template +4 -2
- package/scripts/session/feed.mjs +237 -0
- package/scripts/session/feed.test.mjs +196 -0
- package/scripts/session/supervisor-sh.test.mjs +218 -0
- package/scripts/session/supervisor.mjs +328 -0
- package/scripts/session/supervisor.sh +141 -0
- package/scripts/session/supervisor.test.mjs +482 -0
- package/scripts/setup/configure-macos.sh +250 -55
- package/scripts/setup/configure-macos.test.mjs +306 -0
- package/scripts/setup/init-agent.sh +112 -7
- package/scripts/setup/init-agent.test.mjs +220 -1
- package/scripts/vendor/skill-packs.mjs +354 -0
- package/scripts/vendor/sync-skill-packs.mjs +242 -0
- package/scripts/vendor/sync-skill-packs.test.mjs +103 -0
- package/scripts/watchdog/memory-watchdog.sh +37 -1
- package/scripts/watchdog/memory-watchdog.test.mjs +64 -0
- package/scripts/setup/boot-claude-session.sh +0 -94
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* scripts/session/supervisor.mjs — keeps exactly one main session alive.
|
|
4
|
+
*
|
|
5
|
+
* Launched by launchd (`ai.maestro.<first>-session`, KeepAlive
|
|
6
|
+
* {SuccessfulExit:false}, ThrottleInterval 30) through supervisor.sh. One run
|
|
7
|
+
* of this process is ONE lifetime of the main session:
|
|
8
|
+
*
|
|
9
|
+
* 1. Take the `session` singleton lock (lib/singleton.js). A second
|
|
10
|
+
* supervisor exits 0 — clean — so launchd does not thrash.
|
|
11
|
+
* 2. Pick the multiplexer (tmux if installed, else /usr/bin/screen).
|
|
12
|
+
* 3. If a `maestro-<first>` mux session already exists with no lock holder
|
|
13
|
+
* (an orphan from a killed supervisor), ADOPT it: block until it is gone
|
|
14
|
+
* rather than start a duplicate.
|
|
15
|
+
* 4. Otherwise pre-answer the first-run dialogs an unattended session
|
|
16
|
+
* cannot (lib/session/first-run.mjs seeds ~/.claude.json), then launch
|
|
17
|
+
* `claude --name <first>-main` with the stable session id
|
|
18
|
+
* (`--session-id` first time, `--resume` after) IN THE FOREGROUND of
|
|
19
|
+
* the mux, so this process lives exactly as long as the session. A
|
|
20
|
+
* watchdog flags a session that is alive but has not beaten after
|
|
21
|
+
* HEARTBEAT_GRACE_MS (log + state/session/attention.json for doctor).
|
|
22
|
+
* 5. When the session ends: if a resume died non-zero inside 20 s, rotate
|
|
23
|
+
* to a fresh id (max 3/hour, then sleep 10 min); then exit 75 so launchd
|
|
24
|
+
* relaunches. No cron, no periodic kill — relaunch happens only on death.
|
|
25
|
+
*
|
|
26
|
+
* SIGTERM/SIGINT (launchd stopping the job: `kickstart -k`, `bootout`) are
|
|
27
|
+
* owned HERE, not by the singleton's default handlers: the mux session is
|
|
28
|
+
* told to quit (`screen -X quit` / `tmux kill-session`), the run then ends
|
|
29
|
+
* through the normal path with rotation suppressed (a signal-induced death
|
|
30
|
+
* inside the 20 s window is not a broken transcript), and the exit is 75.
|
|
31
|
+
* Exiting 0 at once — the singleton default — would orphan screen/claude in
|
|
32
|
+
* the supervisor's process group, which launchd then SIGKILLs, leaving the
|
|
33
|
+
* dead socket that step 3 has to wipe.
|
|
34
|
+
*
|
|
35
|
+
* Exit codes: 0 = another supervisor holds the lock (or nothing to supervise);
|
|
36
|
+
* 75 (EX_TEMPFAIL) = the session ended, relaunch me.
|
|
37
|
+
*
|
|
38
|
+
* All effects are injected (`execFile`, fs, clock, sleep, lock) so the whole
|
|
39
|
+
* lifecycle is unit-tested without a terminal, a mux or a `claude` binary.
|
|
40
|
+
* The decisions live in lib/session/*; this file sequences them.
|
|
41
|
+
*
|
|
42
|
+
* @module scripts/session/supervisor
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
import { existsSync as fsExistsSync, readFileSync as fsReadFileSync, mkdirSync as fsMkdirSync, unlinkSync as fsUnlinkSync } from "node:fs";
|
|
46
|
+
import { spawn } from "node:child_process";
|
|
47
|
+
import { homedir } from "node:os";
|
|
48
|
+
import { pathToFileURL } from "node:url";
|
|
49
|
+
|
|
50
|
+
import { resolveAgentRoot } from "../../lib/agent-root.mjs";
|
|
51
|
+
import { writeJsonAtomic as fsWriteJsonAtomic, writeFileAtomic as fsWriteFileAtomic } from "../../lib/fs-atomic.mjs";
|
|
52
|
+
import { parseHeartbeat } from "../../lib/session/liveness.mjs";
|
|
53
|
+
import { ensureClaudeConfig, heartbeatSilence, attentionRecord, HEARTBEAT_GRACE_MS } from "../../lib/session/first-run.mjs";
|
|
54
|
+
import { acquireLock as singletonAcquireLock } from "../../lib/singleton.js";
|
|
55
|
+
import { resolveClaudeBin } from "../../lib/claude-bin.mjs";
|
|
56
|
+
import { sessionPermissionArgs, MAIN_SESSION_SOURCE } from "../../lib/session-permissions.mjs";
|
|
57
|
+
import { loadSessionConfig, sessionPaths } from "../../lib/session/config.mjs";
|
|
58
|
+
import {
|
|
59
|
+
loadMainSession, saveMainSession, newMainSession, agentFirstName, muxSessionName,
|
|
60
|
+
launchMode, recordLaunch, rotationDecision, rotateMainSession, BACKOFF_MS,
|
|
61
|
+
} from "../../lib/session/identity.mjs";
|
|
62
|
+
import { buildClaudeArgs, detectMux, chooseMux, buildMuxCommands, parseScreenList, parseExitFile } from "../../lib/session/launch-args.mjs";
|
|
63
|
+
|
|
64
|
+
/** Another supervisor holds the lock (or the front door is not the session). Clean. */
|
|
65
|
+
export const EXIT_ANOTHER_HOLDS_LOCK = 0;
|
|
66
|
+
/** The session ended; launchd should relaunch (EX_TEMPFAIL). */
|
|
67
|
+
export const EXIT_RELAUNCH = 75;
|
|
68
|
+
/** Name of the singleton lock (state/locks/process/session.pid). */
|
|
69
|
+
export const LOCK_NAME = "session";
|
|
70
|
+
/** How often the tmux `has-session` probe (and the orphan probe) is polled. */
|
|
71
|
+
export const DEFAULT_POLL_MS = 2_000;
|
|
72
|
+
/** The signals the supervisor owns (launchd sends SIGTERM to stop a job). */
|
|
73
|
+
export const OWNED_SIGNALS = Object.freeze(["SIGTERM", "SIGINT"]);
|
|
74
|
+
|
|
75
|
+
/** Real signal wiring: `fn(signal)` on each owned signal. */
|
|
76
|
+
function installSignalHandlers(fn) {
|
|
77
|
+
for (const sig of OWNED_SIGNALS) process.on(sig, () => fn(sig));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Real one-shot timer (unref'd: it must never keep the process alive). Returns cancel. */
|
|
81
|
+
function defaultAfter(ms, fn) {
|
|
82
|
+
const t = setTimeout(fn, ms);
|
|
83
|
+
if (t && typeof t.unref === "function") t.unref();
|
|
84
|
+
return () => clearTimeout(t);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Real `execFile`: spawn without a shell, capture stdout, resolve with the
|
|
89
|
+
* exit code (never reject on a non-zero code — that is a result, not a defect).
|
|
90
|
+
* stderr is inherited so mux/claude diagnostics reach the supervisor log.
|
|
91
|
+
* @returns {Promise<{code:number|null, stdout:string, stderr:string}>}
|
|
92
|
+
*/
|
|
93
|
+
export function execFileResult(file, args, opts = {}) {
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
let child;
|
|
96
|
+
try {
|
|
97
|
+
child = spawn(file, args, { cwd: opts.cwd, env: opts.env || process.env, stdio: ["ignore", "pipe", "inherit"] });
|
|
98
|
+
} catch (err) { reject(err); return; }
|
|
99
|
+
let stdout = "";
|
|
100
|
+
child.stdout.on("data", (d) => { stdout += d; });
|
|
101
|
+
child.on("error", reject);
|
|
102
|
+
child.on("close", (code) => resolve({ code, stdout, stderr: "" }));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Block while `probe` exits 0 (tmux `has-session`); return once it does not.
|
|
110
|
+
* @param {{file:string, args:string[]}} probe
|
|
111
|
+
* @param {{execFile:Function, sleep:Function, pollMs:number}} d
|
|
112
|
+
*/
|
|
113
|
+
export async function waitForProbe(probe, d) {
|
|
114
|
+
for (;;) {
|
|
115
|
+
let r;
|
|
116
|
+
try { r = await d.execFile(probe.file, probe.args); } catch { return; /* probe binary gone → treat as ended */ }
|
|
117
|
+
if (!r || r.code !== 0) return;
|
|
118
|
+
await d.sleep(d.pollMs);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Does the mux session exist right now? */
|
|
123
|
+
async function muxSessionExists(mux, cmds, muxName, d) {
|
|
124
|
+
let r;
|
|
125
|
+
try { r = await d.execFile(cmds.exists.file, cmds.exists.args); } catch { return false; }
|
|
126
|
+
if (mux.kind === "screen") return parseScreenList(r && r.stdout, muxName);
|
|
127
|
+
return !!r && r.code === 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Block until the mux session is gone. */
|
|
131
|
+
async function waitForMuxSession(mux, cmds, muxName, d) {
|
|
132
|
+
if (mux.kind === "tmux") return waitForProbe(cmds.waitProbe, d);
|
|
133
|
+
while (await muxSessionExists(mux, cmds, muxName, d)) await d.sleep(d.pollMs);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* One main-session lifetime. See the module docblock.
|
|
138
|
+
* @param {object} [deps] every effect, injectable; defaults are the real ones
|
|
139
|
+
* @returns {Promise<number>} exit code
|
|
140
|
+
*/
|
|
141
|
+
export async function runSupervisor(deps = {}) {
|
|
142
|
+
const d = {
|
|
143
|
+
agentRoot: deps.agentRoot || resolveAgentRoot(),
|
|
144
|
+
env: deps.env || process.env,
|
|
145
|
+
acquireLock: deps.acquireLock || singletonAcquireLock,
|
|
146
|
+
existsSync: deps.existsSync || fsExistsSync,
|
|
147
|
+
readFileSync: deps.readFileSync || fsReadFileSync,
|
|
148
|
+
writeJsonAtomic: deps.writeJsonAtomic || fsWriteJsonAtomic,
|
|
149
|
+
writeFileAtomic: deps.writeFileAtomic || fsWriteFileAtomic,
|
|
150
|
+
homeDir: deps.homeDir === undefined ? homedir() : deps.homeDir,
|
|
151
|
+
after: deps.after || defaultAfter,
|
|
152
|
+
heartbeatGraceMs: deps.heartbeatGraceMs ?? HEARTBEAT_GRACE_MS,
|
|
153
|
+
mkdirSync: deps.mkdirSync || fsMkdirSync,
|
|
154
|
+
unlinkSync: deps.unlinkSync || fsUnlinkSync,
|
|
155
|
+
now: deps.now || Date.now,
|
|
156
|
+
sleep: deps.sleep || defaultSleep,
|
|
157
|
+
log: deps.log || ((line) => process.stderr.write(`[session-supervisor] ${line}\n`)),
|
|
158
|
+
uuid: deps.uuid,
|
|
159
|
+
execFile: deps.execFile || execFileResult,
|
|
160
|
+
loadConfig: deps.loadConfig || ((root) => loadSessionConfig(root)),
|
|
161
|
+
claudeBin: deps.claudeBin || null,
|
|
162
|
+
permissionArgs: deps.permissionArgs || null,
|
|
163
|
+
pollMs: deps.pollMs ?? DEFAULT_POLL_MS,
|
|
164
|
+
onSignal: deps.onSignal || installSignalHandlers,
|
|
165
|
+
};
|
|
166
|
+
const fsDeps = { readFileSync: d.readFileSync, writeJsonAtomic: d.writeJsonAtomic };
|
|
167
|
+
const agentRoot = d.agentRoot;
|
|
168
|
+
const paths = sessionPaths(agentRoot);
|
|
169
|
+
|
|
170
|
+
// 1. Exactly one. The lock's own signal handlers are declined: SIGTERM is
|
|
171
|
+
// handled below so the mux session is stopped before this process ends.
|
|
172
|
+
if (!d.acquireLock(LOCK_NAME, { signals: false })) {
|
|
173
|
+
d.log("another supervisor holds the session lock — exiting clean");
|
|
174
|
+
return EXIT_ANOTHER_HOLDS_LOCK;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Shutdown on request. `stopCmd` is set once a mux session is ours to stop
|
|
178
|
+
// (adopted or launched); a signal before that only marks the run as
|
|
179
|
+
// stopping so nothing new is launched.
|
|
180
|
+
let stopping = null;
|
|
181
|
+
let stopCmd = null;
|
|
182
|
+
let stopIssued = null;
|
|
183
|
+
const requestStop = () => {
|
|
184
|
+
if (stopIssued || !stopCmd) return;
|
|
185
|
+
stopIssued = Promise.resolve()
|
|
186
|
+
.then(() => d.execFile(stopCmd.file, stopCmd.args))
|
|
187
|
+
.catch((err) => { d.log(`mux stop failed: ${err && err.message ? err.message : err}`); });
|
|
188
|
+
};
|
|
189
|
+
d.onSignal((sig) => {
|
|
190
|
+
if (stopping) return;
|
|
191
|
+
stopping = sig;
|
|
192
|
+
d.log(`${sig} received — stopping the session and exiting ${EXIT_RELAUNCH}`);
|
|
193
|
+
requestStop();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const cfg = await d.loadConfig(agentRoot);
|
|
197
|
+
if (cfg.frontDoor !== "session") {
|
|
198
|
+
d.log(`frontDoor is "${cfg.frontDoor}" — nothing to supervise, exiting clean`);
|
|
199
|
+
return EXIT_ANOTHER_HOLDS_LOCK;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const first = agentFirstName(agentRoot, fsDeps);
|
|
203
|
+
const muxName = muxSessionName(first);
|
|
204
|
+
try { d.mkdirSync(paths.stateDir, { recursive: true }); } catch { /* the atomic writers create it too */ }
|
|
205
|
+
|
|
206
|
+
// 2. Multiplexer.
|
|
207
|
+
const mux = chooseMux(cfg.mux, detectMux({ existsSync: d.existsSync }));
|
|
208
|
+
if (!mux) {
|
|
209
|
+
d.log(`no terminal multiplexer found (tmux or /usr/bin/screen) — backing off ${BACKOFF_MS / 60000} min`);
|
|
210
|
+
await d.sleep(BACKOFF_MS);
|
|
211
|
+
return EXIT_RELAUNCH;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 3. Orphan adoption. We hold the lock, so an existing mux session has no
|
|
215
|
+
// supervisor: attach to its lifetime instead of starting a second one.
|
|
216
|
+
// First clear DEAD sockets (a SIGKILLed screen leaves `(Dead ???)` in
|
|
217
|
+
// SCREENDIR, which `-ls` still lists): `screen -wipe` is hygiene and
|
|
218
|
+
// parseScreenList ignores dead lines regardless, so a failing wipe costs
|
|
219
|
+
// nothing.
|
|
220
|
+
const probeCmds = buildMuxCommands({ mux, muxName, cwd: agentRoot, claudeBin: "claude", claudeArgs: [], exitFile: paths.lastExitFile });
|
|
221
|
+
if (probeCmds.wipe) {
|
|
222
|
+
try { await d.execFile(probeCmds.wipe.file, probeCmds.wipe.args); } catch { /* nothing to wipe, or no permission — the probe decides */ }
|
|
223
|
+
}
|
|
224
|
+
if (await muxSessionExists(mux, probeCmds, muxName, d)) {
|
|
225
|
+
d.log(`adopting orphaned ${mux.kind} session ${muxName} — waiting for it to end`);
|
|
226
|
+
stopCmd = probeCmds.stop;
|
|
227
|
+
if (stopping) requestStop();
|
|
228
|
+
await waitForMuxSession(mux, probeCmds, muxName, d);
|
|
229
|
+
await stopIssued;
|
|
230
|
+
d.log(`adopted session ${muxName} ended — exit ${EXIT_RELAUNCH} for relaunch`);
|
|
231
|
+
return EXIT_RELAUNCH;
|
|
232
|
+
}
|
|
233
|
+
if (stopping) return EXIT_RELAUNCH; // asked to stop before anything was launched
|
|
234
|
+
|
|
235
|
+
// 4. Identity + argv.
|
|
236
|
+
let record = loadMainSession(agentRoot, fsDeps);
|
|
237
|
+
if (!record || cfg.resume === false) record = newMainSession({ now: d.now, uuid: d.uuid });
|
|
238
|
+
const mode = launchMode(record, cfg);
|
|
239
|
+
let permissionArgs = d.permissionArgs;
|
|
240
|
+
if (!permissionArgs) {
|
|
241
|
+
permissionArgs = sessionPermissionArgs({ source: MAIN_SESSION_SOURCE, allowedTools: cfg.allowedTools });
|
|
242
|
+
if (d.env.MAESTRO_SCOPED_PERMISSIONS === "1" && !permissionArgs.includes("--allowedTools")) {
|
|
243
|
+
d.log("MAESTRO_SCOPED_PERMISSIONS=1 but config/session.yaml has no allowedTools — the interactive main session runs with bypass (an unattended session cannot answer permission prompts)");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const claudeArgs = buildClaudeArgs({ first, sessionId: record.sessionId, mode, permissionArgs });
|
|
247
|
+
const cmds = buildMuxCommands({ mux, muxName, cwd: agentRoot, claudeBin: d.claudeBin || resolveClaudeBin(), claudeArgs, exitFile: paths.lastExitFile });
|
|
248
|
+
|
|
249
|
+
// 4b. First-run dialogs. Nobody is attached to press a key, so the two
|
|
250
|
+
// one-time acknowledgements are recorded up front. Fail-open: a seat
|
|
251
|
+
// still launches when this cannot be done — the watchdog below then
|
|
252
|
+
// reports the blocked session instead of nothing at all.
|
|
253
|
+
const seeded = ensureClaudeConfig({
|
|
254
|
+
homeDir: d.homeDir, agentRoot, bypass: permissionArgs.includes("--dangerously-skip-permissions"),
|
|
255
|
+
readFileSync: d.readFileSync, writeFileAtomic: d.writeFileAtomic,
|
|
256
|
+
});
|
|
257
|
+
if (seeded.changed) d.log(`${seeded.path} seeded for an unattended launch: ${seeded.applied.join(", ")}`);
|
|
258
|
+
else if (!seeded.ok) d.log(`${seeded.path || "~/.claude.json"} not seeded (${seeded.error}) — a first-run dialog may block the session; the watchdog will report it`);
|
|
259
|
+
|
|
260
|
+
try { d.unlinkSync(paths.lastExitFile); } catch { /* none from a previous run */ }
|
|
261
|
+
try { d.unlinkSync(paths.attentionFile); } catch { /* none outstanding */ }
|
|
262
|
+
const launched = recordLaunch(record, { now: d.now });
|
|
263
|
+
const saved = saveMainSession(agentRoot, launched, fsDeps);
|
|
264
|
+
if (!saved.ok) d.log(`main-session.json not saved: ${saved.error}`);
|
|
265
|
+
d.log(`launching ${mux.kind} session ${muxName}: claude ${mode === "resume" ? "--resume" : "--session-id"} ${record.sessionId} (resumes=${launched.resumes})`);
|
|
266
|
+
|
|
267
|
+
// 5. Run, in the foreground of this process. The watchdog fires once,
|
|
268
|
+
// after the grace period: a session that has not beaten by then is
|
|
269
|
+
// almost certainly waiting on a dialog or a prompt nobody can see.
|
|
270
|
+
const startedAt = Number(d.now());
|
|
271
|
+
let launchFailed = false;
|
|
272
|
+
stopCmd = cmds.stop;
|
|
273
|
+
const cancelWatchdog = d.after(d.heartbeatGraceMs, () => {
|
|
274
|
+
let hb = null;
|
|
275
|
+
try { hb = parseHeartbeat(d.readFileSync(paths.heartbeatFile, "utf8")); } catch { hb = null; }
|
|
276
|
+
const silence = heartbeatSilence({ startedAt, heartbeat: hb, now: d.now(), graceMs: d.heartbeatGraceMs });
|
|
277
|
+
if (!silence.silent) return;
|
|
278
|
+
const rec = attentionRecord({ reason: silence.reason, mux, muxName, since: startedAt, runMs: silence.runMs });
|
|
279
|
+
try { d.writeJsonAtomic(paths.attentionFile, rec); } catch { /* the log line still says it */ }
|
|
280
|
+
d.log(`session ${muxName} has been up ${Math.round(silence.runMs / 1000)} s with no heartbeat — probably blocked on a first-run dialog or a permission prompt; attach with \`${rec.attach}\` and answer it`);
|
|
281
|
+
});
|
|
282
|
+
try {
|
|
283
|
+
const r = await d.execFile(cmds.start.file, cmds.start.args, { cwd: agentRoot, env: d.env });
|
|
284
|
+
if (r && r.code !== 0) { launchFailed = true; d.log(`${mux.kind} start exited ${r.code}${r.stderr ? `: ${String(r.stderr).trim()}` : ""}`); }
|
|
285
|
+
else if (!cmds.blocking) await waitForProbe(cmds.waitProbe, d);
|
|
286
|
+
} catch (err) {
|
|
287
|
+
launchFailed = true;
|
|
288
|
+
d.log(`${mux.kind} start failed: ${err && err.message ? err.message : err}`);
|
|
289
|
+
}
|
|
290
|
+
const endedAt = Number(d.now());
|
|
291
|
+
try { cancelWatchdog(); } catch { /* already fired */ }
|
|
292
|
+
await stopIssued;
|
|
293
|
+
|
|
294
|
+
let exitCode = null;
|
|
295
|
+
try { exitCode = parseExitFile(d.readFileSync(paths.lastExitFile, "utf8")); } catch { exitCode = null; /* never written */ }
|
|
296
|
+
if (launchFailed && exitCode === null) exitCode = 1;
|
|
297
|
+
d.log(`session ${muxName} ended after ${Math.round((endedAt - startedAt) / 1000)} s (exit ${exitCode === null ? "unknown" : exitCode})`);
|
|
298
|
+
|
|
299
|
+
// 6. Rotation — never for a death we caused.
|
|
300
|
+
if (stopping) {
|
|
301
|
+
d.log(`session ${muxName} stopped on ${stopping} — id kept, exit ${EXIT_RELAUNCH}`);
|
|
302
|
+
return EXIT_RELAUNCH;
|
|
303
|
+
}
|
|
304
|
+
const decision = rotationDecision({ record: launched, mode, exitCode, startedAt, endedAt, now: d.now });
|
|
305
|
+
if (decision.action === "rotate") {
|
|
306
|
+
const rotated = rotateMainSession(launched, { now: d.now, uuid: d.uuid });
|
|
307
|
+
const res = saveMainSession(agentRoot, rotated, fsDeps);
|
|
308
|
+
d.log(`resume of ${launched.sessionId} failed inside the failure window — rotated to ${rotated.sessionId}${res.ok ? "" : ` (not saved: ${res.error})`}`);
|
|
309
|
+
} else if (decision.action === "backoff") {
|
|
310
|
+
d.log(`rotation budget spent for this hour — sleeping ${decision.sleepMs / 60000} min before relaunch`);
|
|
311
|
+
await d.sleep(decision.sleepMs);
|
|
312
|
+
}
|
|
313
|
+
return EXIT_RELAUNCH;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const isMain = (() => {
|
|
317
|
+
try { return process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href; } catch { return false; }
|
|
318
|
+
})();
|
|
319
|
+
|
|
320
|
+
if (isMain) {
|
|
321
|
+
runSupervisor()
|
|
322
|
+
.then((code) => process.exit(code))
|
|
323
|
+
.catch((err) => {
|
|
324
|
+
// A defect in the supervisor itself: log it and ask launchd to try again.
|
|
325
|
+
process.stderr.write(`[session-supervisor] fatal: ${err && err.stack ? err.stack : err}\n`);
|
|
326
|
+
process.exit(EXIT_RELAUNCH);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# supervisor.sh — launchd wrapper for the main-session supervisor.
|
|
3
|
+
#
|
|
4
|
+
# Same idiom as scripts/daemon/launchd-wrapper.sh: launchd's environment is
|
|
5
|
+
# bare (no HOME, PATH, AGENT_ROOT), so we set them here and exec node, rather
|
|
6
|
+
# than putting them in the plist (EX_CONFIG failures seen with symlinked paths).
|
|
7
|
+
#
|
|
8
|
+
# What differs from the daemon wrapper: the SESSION needs the seat's `.env`
|
|
9
|
+
# (CLAUDE_CODE_OAUTH_TOKEN, MAESTRO_PREFER_SUBSCRIPTION_AUTH, MAESTRO_SCOPED_
|
|
10
|
+
# PERMISSIONS, …) in its environment, because `claude` is launched by the
|
|
11
|
+
# supervisor inside a multiplexer and reads its auth from env. The daemon
|
|
12
|
+
# loads .env in JS; the supervisor gets it from here so the same values reach
|
|
13
|
+
# screen/tmux → sh → claude without any of them being written to a plist.
|
|
14
|
+
#
|
|
15
|
+
# .env handling mirrors what dotenv (the daemon's loader) does, so the same
|
|
16
|
+
# file yields the same token here and there: only `KEY=value` lines
|
|
17
|
+
# (optionally `export KEY=value`) are read; comments and malformed lines are
|
|
18
|
+
# skipped; a trailing CR (CRLF file) is dropped; one layer of matching quotes
|
|
19
|
+
# is stripped and a quoted `#` is data; an UNQUOTED value ends at the first
|
|
20
|
+
# `#` and is trimmed; a variable ALREADY in the environment wins (so an
|
|
21
|
+
# operator can override from a shell, and a launchd-provided value is never
|
|
22
|
+
# clobbered). Values are never echoed.
|
|
23
|
+
#
|
|
24
|
+
# Exit codes are the supervisor's: 0 = another supervisor holds the lock,
|
|
25
|
+
# 75 = the session ended, relaunch (KeepAlive SuccessfulExit:false does that).
|
|
26
|
+
#
|
|
27
|
+
# AGENT_ROOT: a value already in the environment (plist EnvironmentVariables,
|
|
28
|
+
# an operator shell) wins; otherwise the agent dir is derived from this
|
|
29
|
+
# script's own location, which is right for the copy `maestro upgrade` places
|
|
30
|
+
# at <agent>/scripts/session/. Either way the root must BE an agent directory
|
|
31
|
+
# (config/agent.json present) — running from the global npm package dir would
|
|
32
|
+
# put locks, state and logs inside node_modules and start claude in the wrong
|
|
33
|
+
# cwd, so that exits 78 (EX_CONFIG) with a message instead.
|
|
34
|
+
#
|
|
35
|
+
# This wrapper is exec'd by ai.maestro.{firstname}-session.plist.
|
|
36
|
+
|
|
37
|
+
set -e
|
|
38
|
+
|
|
39
|
+
if [ -z "${AGENT_ROOT:-}" ] || [ ! -d "${AGENT_ROOT:-}" ]; then
|
|
40
|
+
AGENT_ROOT="$(cd "$(dirname "$0")/../.." && pwd -P)"
|
|
41
|
+
else
|
|
42
|
+
AGENT_ROOT="$(cd "$AGENT_ROOT" && pwd -P)"
|
|
43
|
+
fi
|
|
44
|
+
export AGENT_ROOT
|
|
45
|
+
export AGENT_DIR="$AGENT_ROOT"
|
|
46
|
+
|
|
47
|
+
if [ ! -f "$AGENT_ROOT/config/agent.json" ]; then
|
|
48
|
+
echo "[session-wrapper] FATAL: $AGENT_ROOT is not an agent directory (no config/agent.json) — set AGENT_ROOT or run the copy under <agent>/scripts/session/" >&2
|
|
49
|
+
exit 78
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# ── emergency stop ──────────────────────────────────────────────────────────
|
|
53
|
+
# scripts/emergency-stop.sh drops .emergency-stop and unloads every job; the
|
|
54
|
+
# daemon honours the marker and exits. The session must too — otherwise a
|
|
55
|
+
# by-hand `maestro upgrade` (whose launchd step is held under the marker, but
|
|
56
|
+
# a login re-bootstraps LaunchAgents regardless) would start the front-door
|
|
57
|
+
# claude session under an active stop. Exit 0: KeepAlive {SuccessfulExit:
|
|
58
|
+
# false} does NOT relaunch a clean exit, so launchd stays quiet until the
|
|
59
|
+
# marker is gone and the job is started again (maestro session start).
|
|
60
|
+
if [ -e "$AGENT_ROOT/.emergency-stop" ] || [ -e "$AGENT_ROOT/state/EMERGENCY_STOP" ]; then
|
|
61
|
+
echo "[session-wrapper] emergency stop is active ($AGENT_ROOT/.emergency-stop or state/EMERGENCY_STOP) — not starting the session; remove the marker and run: maestro session start" >&2
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
export HOME="${HOME:-/Users/$(whoami)}"
|
|
65
|
+
export USER="${USER:-$(whoami)}"
|
|
66
|
+
export PATH="$HOME/.local/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
|
67
|
+
|
|
68
|
+
# ── .env → environment ──────────────────────────────────────────────────────
|
|
69
|
+
maestro_source_env() {
|
|
70
|
+
local file="$1" line key value
|
|
71
|
+
[ -f "$file" ] || return 0
|
|
72
|
+
while IFS= read -r line || [ -n "$line" ]; do
|
|
73
|
+
line="${line%$'\r'}"
|
|
74
|
+
line="${line#export }"
|
|
75
|
+
case "$line" in ''|\#*) continue ;; esac
|
|
76
|
+
case "$line" in *=*) ;; *) continue ;; esac
|
|
77
|
+
key="${line%%=*}"
|
|
78
|
+
value="${line#*=}"
|
|
79
|
+
case "$key" in ''|*[!A-Za-z0-9_]*) continue ;; esac
|
|
80
|
+
case "$value" in
|
|
81
|
+
\"*\") value="${value#\"}"; value="${value%\"}" ;;
|
|
82
|
+
\'*\') value="${value#\'}"; value="${value%\'}" ;;
|
|
83
|
+
*)
|
|
84
|
+
value="${value%%#*}"
|
|
85
|
+
value="${value%"${value##*[![:space:]]}"}"
|
|
86
|
+
value="${value#"${value%%[![:space:]]*}"}"
|
|
87
|
+
;;
|
|
88
|
+
esac
|
|
89
|
+
if [ -z "${!key+x}" ]; then
|
|
90
|
+
export "$key=$value"
|
|
91
|
+
fi
|
|
92
|
+
done < "$file"
|
|
93
|
+
}
|
|
94
|
+
maestro_source_env "$AGENT_ROOT/.env"
|
|
95
|
+
|
|
96
|
+
cd "$AGENT_ROOT"
|
|
97
|
+
|
|
98
|
+
# ── node ────────────────────────────────────────────────────────────────────
|
|
99
|
+
# NODE_BIN may be pre-set (tests, operators); otherwise prefer nvm, then
|
|
100
|
+
# homebrew, then system — the daemon wrapper's order. MAESTRO_NODE_CANDIDATES
|
|
101
|
+
# (colon-separated) replaces the fixed list so the search is testable and an
|
|
102
|
+
# unusual install can be pointed at without editing this file.
|
|
103
|
+
NODE_BIN="${NODE_BIN:-}"
|
|
104
|
+
if [ -z "$NODE_BIN" ] || [ ! -x "$NODE_BIN" ]; then
|
|
105
|
+
NODE_BIN=""
|
|
106
|
+
if [ -n "${MAESTRO_NODE_CANDIDATES:-}" ]; then
|
|
107
|
+
IFS=':' read -r -a node_candidates <<< "$MAESTRO_NODE_CANDIDATES"
|
|
108
|
+
else
|
|
109
|
+
node_candidates=(
|
|
110
|
+
"$HOME/.nvm/versions/node/v24.11.1/bin/node"
|
|
111
|
+
"$HOME/.nvm/versions/node/v24/bin/node"
|
|
112
|
+
"$HOME/.nvm/versions/node/v22/bin/node"
|
|
113
|
+
"$HOME/.nvm/versions/node/v20/bin/node"
|
|
114
|
+
/opt/homebrew/bin/node
|
|
115
|
+
/usr/local/bin/node
|
|
116
|
+
/usr/bin/node
|
|
117
|
+
)
|
|
118
|
+
fi
|
|
119
|
+
for candidate in "${node_candidates[@]}"; do
|
|
120
|
+
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
|
|
121
|
+
NODE_BIN="$candidate"
|
|
122
|
+
break
|
|
123
|
+
fi
|
|
124
|
+
done
|
|
125
|
+
fi
|
|
126
|
+
if [ -z "$NODE_BIN" ] && [ -d "$HOME/.nvm/versions/node" ]; then
|
|
127
|
+
NODE_BIN=$(ls -1d "$HOME/.nvm/versions/node"/v*/bin/node 2>/dev/null | sort -V | tail -1)
|
|
128
|
+
fi
|
|
129
|
+
if [ -z "$NODE_BIN" ] || [ ! -x "$NODE_BIN" ]; then
|
|
130
|
+
echo "[session-wrapper] FATAL: could not find node binary" >&2
|
|
131
|
+
exit 127
|
|
132
|
+
fi
|
|
133
|
+
export NODE_BIN
|
|
134
|
+
|
|
135
|
+
# ── exec ────────────────────────────────────────────────────────────────────
|
|
136
|
+
# The supervisor's own diagnostics go to logs/session/; the session itself
|
|
137
|
+
# lives inside the multiplexer and is not captured here.
|
|
138
|
+
SESSION_LOG_DIR="${MAESTRO_SESSION_LOG_DIR:-$AGENT_ROOT/logs/session}"
|
|
139
|
+
mkdir -p "$SESSION_LOG_DIR" 2>/dev/null || true
|
|
140
|
+
SESSION_LOG="$SESSION_LOG_DIR/supervisor-$(date +%Y-%m-%d).log"
|
|
141
|
+
exec "$NODE_BIN" "$AGENT_ROOT/scripts/session/supervisor.mjs" "$@" >> "$SESSION_LOG" 2>&1
|