@cohortapp/agent-sdk 2.17.0 → 2.18.4
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/.claude/settings.json +18 -0
- package/.env.example +18 -5
- package/README.md +1 -0
- package/bin/maestro.mjs +62 -0
- package/docs/guides/billing-console-keys.md +60 -0
- package/docs/guides/front-door-session.md +54 -9
- package/docs/guides/mac-mini.md +20 -25
- package/docs/guides/setup-wizard.md +1 -1
- package/docs/runbooks/fleet-rollout.md +156 -0
- package/docs/runbooks/mac-mini-bootstrap.md +12 -14
- package/lib/action-executor.js +19 -3
- package/lib/budget-guard.mjs +279 -3
- package/lib/channels/base-adapter.mjs +3 -1
- package/lib/channels/contract.mjs +2 -1
- package/lib/channels/inbox-item.mjs +8 -0
- package/lib/claude-bin.mjs +5 -6
- package/lib/cli/doctor-checks.mjs +141 -10
- package/lib/cli/global-setup-extras.mjs +5 -1
- package/lib/cli/inbox.mjs +100 -15
- package/lib/cli/seat-auth.mjs +463 -0
- package/lib/cli/session.mjs +80 -12
- package/lib/collective/capture.mjs +8 -6
- package/lib/collective/global-config.mjs +63 -1
- package/lib/collective/presence.mjs +142 -5
- package/lib/comms/send-gate.mjs +559 -1
- package/lib/diagnostics/alerts.mjs +49 -0
- package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
- package/lib/engine/agents/definitions.mjs +343 -0
- package/lib/engine/agents/persist.mjs +275 -0
- package/lib/engine/agents/runtime.mjs +748 -0
- package/lib/engine/agents/usage.mjs +95 -0
- package/lib/engine/auth-status.mjs +139 -0
- package/lib/engine/budget.mjs +194 -0
- package/lib/engine/cli.mjs +1204 -0
- package/lib/engine/commands/index.mjs +269 -0
- package/lib/engine/context/budget.mjs +219 -0
- package/lib/engine/context/cache.mjs +125 -0
- package/lib/engine/context/child-env.mjs +215 -0
- package/lib/engine/context/compaction.mjs +342 -0
- package/lib/engine/context/images.mjs +90 -0
- package/lib/engine/context/instructions.mjs +327 -0
- package/lib/engine/context/lazy-instructions.mjs +169 -0
- package/lib/engine/context/manager.mjs +182 -0
- package/lib/engine/context/real-path.mjs +91 -0
- package/lib/engine/context/secret-values.mjs +163 -0
- package/lib/engine/context/settings.mjs +274 -0
- package/lib/engine/context/stream-input.mjs +159 -0
- package/lib/engine/guard.mjs +152 -0
- package/lib/engine/hooks.mjs +713 -0
- package/lib/engine/loop.mjs +560 -0
- package/lib/engine/mcp/client.mjs +254 -0
- package/lib/engine/mcp/config.mjs +301 -0
- package/lib/engine/mcp/http.mjs +201 -0
- package/lib/engine/mcp/index.mjs +146 -0
- package/lib/engine/mcp/jsonrpc.mjs +147 -0
- package/lib/engine/mcp/naming.mjs +66 -0
- package/lib/engine/mcp/resources.mjs +89 -0
- package/lib/engine/mcp/results.mjs +133 -0
- package/lib/engine/mcp/stdio.mjs +137 -0
- package/lib/engine/mcp/supervisor.mjs +116 -0
- package/lib/engine/messages.mjs +104 -0
- package/lib/engine/output/json.mjs +164 -0
- package/lib/engine/output/stream-json.mjs +266 -0
- package/lib/engine/permissions.mjs +845 -0
- package/lib/engine/process-identity.mjs +164 -0
- package/lib/engine/process-tree.mjs +551 -0
- package/lib/engine/prompt.mjs +60 -0
- package/lib/engine/session/store.mjs +299 -0
- package/lib/engine/session-runtime/args.mjs +97 -0
- package/lib/engine/session-runtime/host.mjs +143 -0
- package/lib/engine/session-runtime/inbox.mjs +122 -0
- package/lib/engine/session-runtime/notifications.mjs +129 -0
- package/lib/engine/session-runtime/registry.mjs +328 -0
- package/lib/engine/session-runtime/runner.mjs +344 -0
- package/lib/engine/session-runtime/socket.mjs +212 -0
- package/lib/engine/session-runtime/wakeup.mjs +115 -0
- package/lib/engine/skills/index.mjs +321 -0
- package/lib/engine/tools/bash-background.mjs +533 -0
- package/lib/engine/tools/bash.mjs +216 -0
- package/lib/engine/tools/edit.mjs +97 -0
- package/lib/engine/tools/glob.mjs +81 -0
- package/lib/engine/tools/grep.mjs +224 -0
- package/lib/engine/tools/index.mjs +84 -0
- package/lib/engine/tools/list-agents.mjs +32 -0
- package/lib/engine/tools/ls.mjs +127 -0
- package/lib/engine/tools/monitor.mjs +82 -0
- package/lib/engine/tools/notebook-edit.mjs +218 -0
- package/lib/engine/tools/read.mjs +103 -0
- package/lib/engine/tools/schedule-wakeup.mjs +45 -0
- package/lib/engine/tools/schema.mjs +144 -0
- package/lib/engine/tools/send-message.mjs +77 -0
- package/lib/engine/tools/session.mjs +70 -0
- package/lib/engine/tools/todo.mjs +144 -0
- package/lib/engine/tools/toolsearch.mjs +217 -0
- package/lib/engine/tools/walk.mjs +193 -0
- package/lib/engine/tools/web-switch.mjs +31 -0
- package/lib/engine/tools/webfetch-html.mjs +387 -0
- package/lib/engine/tools/webfetch-net.mjs +340 -0
- package/lib/engine/tools/webfetch.mjs +198 -0
- package/lib/engine/tools/websearch.mjs +91 -0
- package/lib/engine/tools/workflow.mjs +95 -0
- package/lib/engine/tools/write.mjs +76 -0
- package/lib/engine/tui/line-editor.mjs +137 -0
- package/lib/engine/tui/render.mjs +86 -0
- package/lib/engine/tui/tui.mjs +274 -0
- package/lib/engine/wire/anthropic-messages.mjs +263 -0
- package/lib/engine/wire/effort.mjs +36 -0
- package/lib/engine/wire/errors.mjs +496 -0
- package/lib/engine/wire/http.mjs +441 -0
- package/lib/engine/wire/index.mjs +76 -0
- package/lib/engine/wire/openai-chat.mjs +332 -0
- package/lib/engine/wire/prompt-cache.mjs +79 -0
- package/lib/engine/wire/search.mjs +140 -0
- package/lib/engine/wire/sse.mjs +114 -0
- package/lib/engine/wire/stall.mjs +349 -0
- package/lib/engine/wire/token-provider.mjs +175 -0
- package/lib/engine/wire/usage.mjs +192 -0
- package/lib/engine/workflow/host.mjs +524 -0
- package/lib/engine/workflow/journal.mjs +188 -0
- package/lib/engine/workflow/json-schema.mjs +171 -0
- package/lib/engine/workflow/meta.mjs +329 -0
- package/lib/engine/workflow/notifications.mjs +52 -0
- package/lib/engine/workflow/runtime.mjs +447 -0
- package/lib/engine/workflow/sandbox.mjs +534 -0
- package/lib/engine/workflow/worker.mjs +141 -0
- package/lib/engine/workflow/worktree.mjs +74 -0
- package/lib/execution/disposition.mjs +1 -1
- package/lib/execution/intake.mjs +10 -0
- package/lib/execution/surface-policy.mjs +15 -0
- package/lib/learning/curator.mjs +8 -6
- package/lib/learning/reflect.mjs +8 -6
- package/lib/model-router/catalog/cohort.yaml +137 -0
- package/lib/model-router/catalog.mjs +118 -1
- package/lib/model-router/failover.mjs +67 -16
- package/lib/model-router/llm-task.mjs +39 -3
- package/lib/model-router/resolve.mjs +89 -3
- package/lib/model-router/spawn.mjs +46 -47
- package/lib/model-router/taxonomy.mjs +126 -4
- package/lib/org/cost-sync.mjs +141 -11
- package/lib/org/inbound/broadcast.mjs +289 -0
- package/lib/org/inbound/collective.mjs +375 -0
- package/lib/org/inbound/directedness.mjs +96 -8
- package/lib/org/inbound/facts.mjs +78 -2
- package/lib/org/inbound/project.mjs +22 -0
- package/lib/org/inbound/surfaces.mjs +14 -0
- package/lib/org/llm-token.mjs +879 -0
- package/lib/org/mesh.mjs +61 -0
- package/lib/org/messaging.mjs +3 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +15 -0
- package/lib/org/quota.mjs +520 -0
- package/lib/org/tool-surface.mjs +104 -16
- package/lib/org/ui-parity.mjs +16 -1
- package/lib/org/work-ledger.mjs +37 -6
- package/lib/rate-guard.mjs +114 -1
- package/lib/resource-governor.mjs +41 -6
- package/lib/runtime/adapter.mjs +823 -0
- package/lib/runtime/child-env.mjs +191 -0
- package/lib/runtime/legacy-shell-guard.mjs +97 -0
- package/lib/runtime/seat-engine.mjs +162 -0
- package/lib/session/ask-ledger.mjs +271 -0
- package/lib/session/current-work.mjs +676 -0
- package/lib/session/feed-core.mjs +40 -3
- package/lib/session/launch-args.mjs +56 -4
- package/lib/session/status-summary.mjs +26 -9
- package/lib/session/upgrade-notice.mjs +42 -0
- package/lib/setup/claude-probe.mjs +117 -13
- package/lib/setup/enrich.mjs +13 -10
- package/lib/setup/sections/model.mjs +39 -13
- package/lib/telemetry/collect.mjs +208 -9
- package/lib/upgrade/ignored-drift.mjs +105 -0
- package/lib/voice/post-call-brief.mjs +30 -17
- package/package.json +13 -3
- package/plugins/maestro-skills/skills/board-work.md +5 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
- package/plugins/maestro-skills/skills/main-session.md +18 -7
- package/scripts/ci/check-tarball-fidelity.mjs +126 -2
- package/scripts/ci/run-tests.mjs +47 -19
- package/scripts/cohort-llm/api-key-helper.mjs +92 -0
- package/scripts/collective/hook-runner.mjs +29 -2
- package/scripts/continuous-monitor.sh +13 -0
- package/scripts/cost/track-claude-usage.mjs +15 -0
- package/scripts/daemon/agent-daemon.mjs +408 -20
- package/scripts/daemon/assurance.mjs +48 -12
- package/scripts/daemon/cadence-consumer.mjs +218 -68
- package/scripts/daemon/cadence-handlers.mjs +73 -4
- package/scripts/daemon/classifier.mjs +75 -26
- package/scripts/daemon/context-compiler.mjs +51 -37
- package/scripts/daemon/deliver.mjs +30 -1
- package/scripts/daemon/dispatcher.mjs +595 -149
- package/scripts/daemon/health.mjs +14 -1
- package/scripts/daemon/maestro-daemon.mjs +11 -0
- package/scripts/daemon/prompt-builder.mjs +24 -0
- package/scripts/daemon/responder.mjs +246 -79
- package/scripts/daemon/sdk-version.mjs +98 -16
- package/scripts/eval/probe-gateway.mjs +635 -0
- package/scripts/eval/replay/extract.mjs +270 -0
- package/scripts/eval/replay/grade.mjs +260 -0
- package/scripts/eval/replay/lib/config.mjs +50 -0
- package/scripts/eval/replay/lib/effects.mjs +65 -0
- package/scripts/eval/replay/lib/fixture.mjs +188 -0
- package/scripts/eval/replay/lib/judge.mjs +72 -0
- package/scripts/eval/replay/lib/redact.mjs +136 -0
- package/scripts/eval/replay/lib/sandbox.mjs +170 -0
- package/scripts/eval/replay/lib/schema-check.mjs +63 -0
- package/scripts/eval/replay/lib/transcript.mjs +76 -0
- package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
- package/scripts/eval/replay/report.mjs +185 -0
- package/scripts/eval/replay/run.mjs +404 -0
- package/scripts/fleet/rollout.mjs +1094 -0
- package/scripts/hooks/pre-send-audit.sh +36 -245
- package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
- package/scripts/hooks/validate-state-yaml.sh +190 -0
- package/scripts/huddle/huddle-llm.mjs +361 -0
- package/scripts/huddle/huddle-server.mjs +46 -121
- package/scripts/local-triggers/autoupdate.sh +448 -78
- package/scripts/local-triggers/run-trigger.sh +13 -0
- package/scripts/maintenance/pin-integrity.mjs +364 -0
- package/scripts/poll-slack-events.sh +41 -9
- package/scripts/poller/slack-socket-mode.mjs +28 -3
- package/scripts/session/supervisor.mjs +80 -13
- package/scripts/spawn-session.sh +13 -0
- package/bin/maestro.test.mjs +0 -1574
- package/lib/action-executor.test.mjs +0 -871
- package/lib/archetype.test.mjs +0 -132
- package/lib/assurance/plan-note.test.mjs +0 -234
- package/lib/assurance/room-budget.test.mjs +0 -486
- package/lib/assurance/tier.test.mjs +0 -174
- package/lib/autonomy.test.mjs +0 -66
- package/lib/backlog.test.mjs +0 -302
- package/lib/backup/policy.test.mjs +0 -305
- package/lib/budget-escalate.test.mjs +0 -232
- package/lib/budget-guard.envelope.test.mjs +0 -476
- package/lib/budget-guard.test.mjs +0 -427
- package/lib/cadence-bus-requeue.test.mjs +0 -83
- package/lib/cadence-bus-schedule.test.mjs +0 -194
- package/lib/cadence-bus.test.mjs +0 -720
- package/lib/cadences.test.mjs +0 -230
- package/lib/capability/inventory.test.mjs +0 -232
- package/lib/capability.test.mjs +0 -78
- package/lib/channels/base-adapter.test.mjs +0 -590
- package/lib/channels/channels.test.mjs +0 -371
- package/lib/channels/contract.test.mjs +0 -162
- package/lib/channels/inbox-item.test.mjs +0 -368
- package/lib/channels/orgmail/adapter.test.mjs +0 -448
- package/lib/channels/pairing.test.mjs +0 -270
- package/lib/channels/repeat-suppressor.test.mjs +0 -134
- package/lib/channels/slack-adapter.test.mjs +0 -212
- package/lib/channels/telegram-adapter.test.mjs +0 -306
- package/lib/channels/voice/adapter.test.mjs +0 -278
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
- package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
- package/lib/charter.test.mjs +0 -89
- package/lib/claude-bin.test.mjs +0 -131
- package/lib/cli/board.test.mjs +0 -227
- package/lib/cli/design.test.mjs +0 -270
- package/lib/cli/doctor-checks.test.mjs +0 -336
- package/lib/cli/global-setup-extras.test.mjs +0 -462
- package/lib/cli/inbox.test.mjs +0 -230
- package/lib/cli/session-ack.test.mjs +0 -63
- package/lib/cli/session.test.mjs +0 -613
- package/lib/collective/capture.test.mjs +0 -121
- package/lib/collective/cards.test.mjs +0 -114
- package/lib/collective/config.test.mjs +0 -123
- package/lib/collective/global-config.test.mjs +0 -220
- package/lib/collective/global-skills.test.mjs +0 -126
- package/lib/collective/presence.test.mjs +0 -95
- package/lib/collective/recall.test.mjs +0 -116
- package/lib/collective/vendor-skills.test.mjs +0 -306
- package/lib/comms/send-gate.test.mjs +0 -770
- package/lib/comms.test.mjs +0 -41
- package/lib/context/budget.test.mjs +0 -252
- package/lib/context/history-scope.test.mjs +0 -79
- package/lib/cost/ledger-row.test.mjs +0 -183
- package/lib/design/design-md.test.mjs +0 -318
- package/lib/design/fixtures/DESIGN.golden.md +0 -238
- package/lib/design/fixtures/PRODUCT.golden.md +0 -67
- package/lib/design/fixtures/foundation.json +0 -133
- package/lib/design/refresh-gate.test.mjs +0 -144
- package/lib/design/write.test.mjs +0 -241
- package/lib/diagnostics/alerts.test.mjs +0 -318
- package/lib/diagnostics/backup-freshness.test.mjs +0 -185
- package/lib/diagnostics/counters.test.mjs +0 -206
- package/lib/diagnostics/events.test.mjs +0 -290
- package/lib/diagnostics/otel.test.mjs +0 -196
- package/lib/diagnostics/trace.test.mjs +0 -251
- package/lib/env-compat.test.mjs +0 -104
- package/lib/execution/disposition.test.mjs +0 -553
- package/lib/execution/drive.test.mjs +0 -270
- package/lib/execution/effects.test.mjs +0 -344
- package/lib/execution/intake.test.mjs +0 -389
- package/lib/execution/journal.test.mjs +0 -261
- package/lib/execution/match.test.mjs +0 -235
- package/lib/execution/pipeline.test.mjs +0 -392
- package/lib/execution/route.test.mjs +0 -186
- package/lib/execution/surface-policy.test.mjs +0 -162
- package/lib/fs-atomic.test.mjs +0 -72
- package/lib/fs-ownership.test.mjs +0 -158
- package/lib/goals/admission.test.mjs +0 -164
- package/lib/goals/classify.test.mjs +0 -167
- package/lib/goals/collaborate.test.mjs +0 -336
- package/lib/goals/gaps.test.mjs +0 -284
- package/lib/goals/loop.test.mjs +0 -845
- package/lib/hooks/bus.test.mjs +0 -387
- package/lib/identity/persona.test.mjs +0 -142
- package/lib/kpi-sensors.test.mjs +0 -278
- package/lib/kpi.test.mjs +0 -244
- package/lib/learning/config.test.mjs +0 -75
- package/lib/learning/counters.test.mjs +0 -69
- package/lib/learning/curator-consolidate.test.mjs +0 -238
- package/lib/learning/curator.test.mjs +0 -106
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.test.mjs +0 -125
- package/lib/learning/skill-writer.test.mjs +0 -210
- package/lib/mandate/audit.test.mjs +0 -195
- package/lib/mandate/contract.test.mjs +0 -185
- package/lib/mandate/derive.test.mjs +0 -274
- package/lib/mandate/model.test.mjs +0 -164
- package/lib/mandate/refresh.test.mjs +0 -389
- package/lib/mcp/server.test.mjs +0 -426
- package/lib/model-router/auth-profiles.test.mjs +0 -580
- package/lib/model-router/catalog.test.mjs +0 -385
- package/lib/model-router/economics.test.mjs +0 -438
- package/lib/model-router/failover.test.mjs +0 -439
- package/lib/model-router/health.test.mjs +0 -338
- package/lib/model-router/integration-coverage.test.mjs +0 -831
- package/lib/model-router/integration.test.mjs +0 -564
- package/lib/model-router/ledger.test.mjs +0 -415
- package/lib/model-router/llm-task.test.mjs +0 -392
- package/lib/model-router/org-credentials.test.mjs +0 -265
- package/lib/model-router/pricing-refresh.test.mjs +0 -286
- package/lib/model-router/reconcile.test.mjs +0 -316
- package/lib/model-router/repair.test.mjs +0 -180
- package/lib/model-router/spawn.test.mjs +0 -446
- package/lib/model-router/taxonomy.test.mjs +0 -410
- package/lib/model-router.test.mjs +0 -1207
- package/lib/org/activity.test.mjs +0 -134
- package/lib/org/approvals.test.mjs +0 -216
- package/lib/org/awareness.test.mjs +0 -159
- package/lib/org/board-mine-cache.test.mjs +0 -53
- package/lib/org/board.test.mjs +0 -187
- package/lib/org/bootstrap-context.test.mjs +0 -153
- package/lib/org/client.test.mjs +0 -1206
- package/lib/org/cohort-client.test.mjs +0 -126
- package/lib/org/cost-sync.test.mjs +0 -153
- package/lib/org/doctor.test.mjs +0 -346
- package/lib/org/engagement-ledger.test.mjs +0 -112
- package/lib/org/engagement.test.mjs +0 -739
- package/lib/org/handoff.test.mjs +0 -269
- package/lib/org/inbound/directedness.test.mjs +0 -668
- package/lib/org/inbound/facts.test.mjs +0 -471
- package/lib/org/inbound/hydrate.test.mjs +0 -908
- package/lib/org/inbound/index.test.mjs +0 -429
- package/lib/org/inbound/project.test.mjs +0 -287
- package/lib/org/integration-tools.test.mjs +0 -160
- package/lib/org/keys.test.mjs +0 -92
- package/lib/org/knowledge.test.mjs +0 -326
- package/lib/org/leases.test.mjs +0 -235
- package/lib/org/mesh-directives.test.mjs +0 -110
- package/lib/org/mesh-integration.test.mjs +0 -127
- package/lib/org/mesh.test.mjs +0 -400
- package/lib/org/messaging.test.mjs +0 -471
- package/lib/org/param-contract.test.mjs +0 -477
- package/lib/org/policy.test.mjs +0 -237
- package/lib/org/protocol.checksum.test.mjs +0 -90
- package/lib/org/protocol.test.mjs +0 -323
- package/lib/org/push.test.mjs +0 -792
- package/lib/org/registry.test.mjs +0 -100
- package/lib/org/resource-tools.test.mjs +0 -361
- package/lib/org/tool-access.test.mjs +0 -144
- package/lib/org/tool-surface-integration.test.mjs +0 -120
- package/lib/org/tool-surface.test.mjs +0 -1268
- package/lib/org/typing.test.mjs +0 -291
- package/lib/org/ui-parity.test.mjs +0 -560
- package/lib/org/verify.test.mjs +0 -194
- package/lib/org/work-ledger.test.mjs +0 -273
- package/lib/plan/adoption-e2e.test.mjs +0 -366
- package/lib/plan/budget-enforcement.test.mjs +0 -400
- package/lib/plan/compile.test.mjs +0 -382
- package/lib/plan/emit.test.mjs +0 -269
- package/lib/plan/explain.test.mjs +0 -188
- package/lib/prompts/parallelism.test.mjs +0 -177
- package/lib/rag/rag.test.mjs +0 -505
- package/lib/rate-guard.test.mjs +0 -272
- package/lib/reactive-gate.test.mjs +0 -57
- package/lib/render.test.mjs +0 -68
- package/lib/resource-governor.test.mjs +0 -488
- package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
- package/lib/scheduling/jitter.test.mjs +0 -140
- package/lib/secrets/broker.test.mjs +0 -280
- package/lib/secrets/providers.test.mjs +0 -274
- package/lib/security/audit-engine.test.mjs +0 -424
- package/lib/security/coerce-args.test.mjs +0 -281
- package/lib/security/dangerous-tools.test.mjs +0 -68
- package/lib/security/external-content.test.mjs +0 -84
- package/lib/security/redact.test.mjs +0 -441
- package/lib/security/secret-equal.test.mjs +0 -55
- package/lib/session/config.test.mjs +0 -92
- package/lib/session/feed-core.test.mjs +0 -198
- package/lib/session/first-run.test.mjs +0 -121
- package/lib/session/frontdoor.test.mjs +0 -205
- package/lib/session/handoffs.test.mjs +0 -183
- package/lib/session/identity.test.mjs +0 -180
- package/lib/session/inbox-claims.test.mjs +0 -286
- package/lib/session/launch-args.test.mjs +0 -157
- package/lib/session/liveness.test.mjs +0 -100
- package/lib/session/status-summary.test.mjs +0 -118
- package/lib/session-permissions.test.mjs +0 -120
- package/lib/setup/claude-probe.test.mjs +0 -187
- package/lib/setup/completeness.test.mjs +0 -110
- package/lib/setup/context-pack.test.mjs +0 -89
- package/lib/setup/enrich.test.mjs +0 -115
- package/lib/setup/enroll-from-cohort.test.mjs +0 -300
- package/lib/setup/integration.test.mjs +0 -162
- package/lib/setup/io.test.mjs +0 -77
- package/lib/setup/runner.test.mjs +0 -132
- package/lib/setup/sections/identity.test.mjs +0 -234
- package/lib/setup/sections/inventory.test.mjs +0 -198
- package/lib/setup/sections/learning.test.mjs +0 -81
- package/lib/setup/sections/mandate.test.mjs +0 -388
- package/lib/setup/sections/messaging.test.mjs +0 -127
- package/lib/setup/sections/model.test.mjs +0 -240
- package/lib/setup/sections/org.test.mjs +0 -346
- package/lib/setup/sections/orgmail.test.mjs +0 -118
- package/lib/setup/sections/recovery.test.mjs +0 -98
- package/lib/setup/sections/subagents.test.mjs +0 -429
- package/lib/setup/sections/verify.test.mjs +0 -175
- package/lib/setup/sot.test.mjs +0 -81
- package/lib/setup/state.test.mjs +0 -115
- package/lib/singleton.test.mjs +0 -151
- package/lib/subagents/cli.test.mjs +0 -389
- package/lib/subagents/client.test.mjs +0 -309
- package/lib/subagents/gap.test.mjs +0 -234
- package/lib/subagents/lock.test.mjs +0 -248
- package/lib/subagents/manifest.test.mjs +0 -175
- package/lib/subagents/refs.test.mjs +0 -204
- package/lib/subagents/resolve.test.mjs +0 -422
- package/lib/subagents/schema.test.mjs +0 -328
- package/lib/telemetry/alerts.test.mjs +0 -109
- package/lib/telemetry/collect.test.mjs +0 -1274
- package/lib/tool-definitions-integration.test.mjs +0 -83
- package/lib/tool-definitions.test.mjs +0 -437
- package/lib/upgrade/global-refresh.test.mjs +0 -65
- package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
- package/lib/upgrade/post-steps.test.mjs +0 -200
- package/lib/upgrade/verify.test.mjs +0 -164
- package/lib/util/fetch-timeout.test.mjs +0 -202
- package/lib/util/reconnect.test.mjs +0 -369
- package/lib/util/unhandled.test.mjs +0 -216
- package/lib/voice/outbound.test.mjs +0 -69
- package/lib/voice/session-rotation.test.mjs +0 -114
- package/lib/voice/stt.test.mjs +0 -226
- package/lib/voice/voice.test.mjs +0 -990
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
- package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
- package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
- package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
- package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
- package/scripts/ci/check-skill-packs.test.mjs +0 -495
- package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
- package/scripts/ci/check.test.mjs +0 -194
- package/scripts/ci/conformance-org-api.test.mjs +0 -425
- package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
- package/scripts/collective/hook-runner.test.mjs +0 -173
- package/scripts/cost/fleet-digest.test.mjs +0 -207
- package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
- package/scripts/cost/track-claude-usage.test.mjs +0 -148
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
- package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
- package/scripts/daemon/agent-daemon.test.mjs +0 -995
- package/scripts/daemon/assurance-e2e.test.mjs +0 -613
- package/scripts/daemon/assurance.test.mjs +0 -1791
- package/scripts/daemon/board-mirror.test.mjs +0 -165
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
- package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
- package/scripts/daemon/cadence-consumer.test.mjs +0 -776
- package/scripts/daemon/cadence-handlers.test.mjs +0 -837
- package/scripts/daemon/classifier-identity.test.mjs +0 -137
- package/scripts/daemon/classifier.test.mjs +0 -266
- package/scripts/daemon/classify-kind.test.mjs +0 -40
- package/scripts/daemon/context-compiler.test.mjs +0 -406
- package/scripts/daemon/deliver.test.mjs +0 -564
- package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
- package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
- package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
- package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
- package/scripts/daemon/execution-ladder.test.mjs +0 -470
- package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
- package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
- package/scripts/daemon/inbox-deferral.test.mjs +0 -336
- package/scripts/daemon/inbox-wake.test.mjs +0 -199
- package/scripts/daemon/integration.test.mjs +0 -149
- package/scripts/daemon/lib/self-echo.test.mjs +0 -153
- package/scripts/daemon/lib/session-router.test.mjs +0 -554
- package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
- package/scripts/daemon/prompt-builder.test.mjs +0 -556
- package/scripts/daemon/responder-cost.test.mjs +0 -68
- package/scripts/daemon/responder-history.test.mjs +0 -221
- package/scripts/daemon/sdk-version.test.mjs +0 -31
- package/scripts/daemon/session-lock.test.mjs +0 -252
- package/scripts/daemon/session-outcomes.test.mjs +0 -533
- package/scripts/daemon/typing-registry.test.mjs +0 -102
- package/scripts/hooks/pre-send-audit.test.mjs +0 -354
- package/scripts/huddle/huddle-prompt.test.mjs +0 -176
- package/scripts/local-triggers/autoupdate.test.mjs +0 -518
- package/scripts/local-triggers/generate-plists.test.mjs +0 -456
- package/scripts/media-generation/brand-clause.test.mjs +0 -135
- package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
- package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
- package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
- package/scripts/poller/slack-socket-mode.test.mjs +0 -805
- package/scripts/poller-launchd/install.test.mjs +0 -243
- package/scripts/restore-from-backup.test.mjs +0 -181
- package/scripts/session/feed.test.mjs +0 -196
- package/scripts/session/supervisor-sh.test.mjs +0 -218
- package/scripts/session/supervisor.test.mjs +0 -482
- package/scripts/setup/configure-macos.test.mjs +0 -306
- package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
- package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
- package/scripts/setup/generate-capability.test.mjs +0 -134
- package/scripts/setup/init-agent.test.mjs +0 -370
- package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
- package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
- package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
|
@@ -30,19 +30,101 @@
|
|
|
30
30
|
# state/autoupdate/last.json remembers it and the run holds for 24 h
|
|
31
31
|
# (MAESTRO_AUTOUPDATE_FAILED_HOLD_S; delete last.json to retry now).
|
|
32
32
|
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
# alive
|
|
37
|
-
#
|
|
38
|
-
#
|
|
33
|
+
# THE HEALTH GATE means HEALTH, not "running". It used to be "daemon pid alive
|
|
34
|
+
# + a fresh 'org-mesh connected' / '[daemon] Running' line in today's log" —
|
|
35
|
+
# and a CRASH-LOOPING daemon satisfies both: launchd respawns it, so it is
|
|
36
|
+
# alive at any instant, and every respawn prints the connect line again. One
|
|
37
|
+
# seat logged "up to date (2.17.0)" every hour for SIX DAYS (2026-09-15..21)
|
|
38
|
+
# while its daemon died ~25 s after every start; the org saw no presence beat
|
|
39
|
+
# from it for ten days and nothing on the box said a word. The gate now asks
|
|
40
|
+
# four questions, and the daemon must answer all of them (is_healthy):
|
|
41
|
+
#
|
|
42
|
+
# (a) PID STABILITY — the set of daemon pids is unchanged across two samples
|
|
43
|
+
# STABLE_S apart AND every pid's `ps etime` is ≥ STABLE_S. STABLE_S is 90 s
|
|
44
|
+
# (MAESTRO_AUTOUPDATE_STABLE_S): 3× the observed ~25 s crash interval, so a
|
|
45
|
+
# loop cannot straddle both samples by luck, and well inside the hourly
|
|
46
|
+
# cadence. A 25 s loop fails on the second sample and on uptime.
|
|
47
|
+
# (b) NO FATAL LINE in the daemon's own log since THIS daemon booted: the
|
|
48
|
+
# lines lib/util/unhandled.mjs and agent-daemon.mjs write on the way down
|
|
49
|
+
# ("[DAEMON] uncaughtException:", "[daemon] Fatal:"), the wrapper's
|
|
50
|
+
# "[wrapper] FATAL:", and Node's ERR_MODULE_NOT_FOUND (a bad release's
|
|
51
|
+
# import failure). The log carries no per-line timestamps, the wrapper
|
|
52
|
+
# names the file for the day the process STARTED and APPENDS to it, so
|
|
53
|
+
# one file holds every incarnation that started that day. The scan
|
|
54
|
+
# starts at the later of: the line count captured before a kickstart,
|
|
55
|
+
# and the running daemon's own boot marker — "[DAEMON] boot pid=<pid>"
|
|
56
|
+
# (maestro-daemon.mjs, from the release that added this gate), else the last "[daemon] Running" /
|
|
57
|
+
# "org-mesh connected" line. A crash earlier the same day that launchd
|
|
58
|
+
# (or an operator) already recovered from is history, not health. A
|
|
59
|
+
# missing log file has no fatal lines (fail-open here — (a) and (c)
|
|
60
|
+
# carry the gate).
|
|
61
|
+
# (c) A SERVER-ACKNOWLEDGED BEAT — state/org/last-beat.json {at, ok, code?,
|
|
62
|
+
# sdkVersion?}, which lib/org/mesh.mjs overwrites on EVERY presence beat
|
|
63
|
+
# (ok:true only when the org answered 2xx; a rejected or unreachable beat
|
|
64
|
+
# writes ok:false). The record must say ok:true, be dated after the
|
|
65
|
+
# daemon's start, and be younger than BEAT_FRESH_S (300 s,
|
|
66
|
+
# MAESTRO_AUTOUPDATE_BEAT_FRESH_S — the same 5 min after which hq's
|
|
67
|
+
# presence view calls a seat offline). When the record carries
|
|
68
|
+
# `sdkVersion` — the version that beat TOLD the org, which hq persists to
|
|
69
|
+
# AgentStatus.machine.sdkVersion — it must equal the installed version:
|
|
70
|
+
# an accepted beat that reported the previous version proves the old
|
|
71
|
+
# process is alive, not that the org sees the new one. A record without
|
|
72
|
+
# the field (a mesh older than this check, or a beat whose status
|
|
73
|
+
# collector failed) is liveness proof only and is accepted as before.
|
|
74
|
+
# Skipped only when config/org.yaml does not enroll this seat (an
|
|
75
|
+
# un-enrolled daemon never beats).
|
|
76
|
+
# (d) The UP-TO-DATE path is not exempt. When the installed version is already
|
|
77
|
+
# @latest the run still runs (a)-(c); an unhealthy daemon is logged
|
|
78
|
+
# loudly, written to state/autoupdate/last.json {healthy:false, reason:
|
|
79
|
+
# "unhealthy-current: …"} (the beat carries it as machine.upgrade), and —
|
|
80
|
+
# once per unhealthy episode, on the transition — announced through
|
|
81
|
+
# state/session/upgrade-notice.json {from:CUR,to:CUR,healthy:false,reason}
|
|
82
|
+
# so the front-door session's feed surfaces it as a `daemon-unhealthy`
|
|
83
|
+
# directive (never `upgrade-available` — nothing to restart onto).
|
|
84
|
+
# Nothing is rolled back: there is no upgrade to undo. A recovery
|
|
85
|
+
# restores last.json.
|
|
86
|
+
# (f) AHEAD OF THE REGISTRY is reported, not silently read as current. When
|
|
87
|
+
# the installed version is strictly GREATER than npm's latest, this build
|
|
88
|
+
# was never published: it exists on this box and nowhere else, and every
|
|
89
|
+
# other seat — which follows @latest — will never receive it. The
|
|
90
|
+
# up-to-date branch used to swallow that case into "up to date (<CUR>)".
|
|
91
|
+
# That is exactly how THIS gate came to be running on zero machines: the
|
|
92
|
+
# seat that wrote it installed 2.18.0 locally, npm's latest stayed at
|
|
93
|
+
# 2.17.0, and fifteen boxes logged a reassuring hourly line while the
|
|
94
|
+
# crash-loop gate reached none of them. Now it is logged loudly and
|
|
95
|
+
# written to state/autoupdate/last.json {healthy:true, reason:
|
|
96
|
+
# "ahead-of-registry: npm latest is <LATEST>"} — healthy because the
|
|
97
|
+
# DAEMON is, reason because the FLEET is not — which the beat carries to
|
|
98
|
+
# the org as machine.upgrade. It never blocks, never rolls back and never
|
|
99
|
+
# writes a session notice: there is no upgrade to restart onto, and the
|
|
100
|
+
# fix (publish, or roll this seat back) is a human's.
|
|
101
|
+
# (e) A STALE DAEMON is reconciled on the up-to-date path. A hand-run
|
|
102
|
+
# `maestro upgrade` installs the package and reconciles the seat but
|
|
103
|
+
# never restarts the daemon (post-steps.mjs: "restarted by autoupdate,
|
|
104
|
+
# never here"; its verify row promises the new version "takes effect
|
|
105
|
+
# when autoupdate kickstarts it"). When state/dashboards/daemon-health.yaml
|
|
106
|
+
# says the LIVE daemon (its pid is one of ours) started on a different
|
|
107
|
+
# sdk_version than the installed one, this run kickstarts it, waits, and
|
|
108
|
+
# runs the gate exactly as after an automatic install — healthy →
|
|
109
|
+
# upgrade-notice {from:<old>,to:CUR} + last.json, as for any completed
|
|
110
|
+
# hop. Without this a manual upgrade ran old daemon code until the next
|
|
111
|
+
# release, and the gate misread its missing beat record as unhealthy.
|
|
112
|
+
#
|
|
113
|
+
# When the generated plist set carries a -session job, the gate ALSO checks the
|
|
114
|
+
# session label is loaded and a supervisor is alive — but a missing session is
|
|
115
|
+
# logged as `reconcile-failed` and does NOT roll back (the daemon's --print lane
|
|
116
|
+
# is the front door meanwhile; doctor and `maestro upgrade --verify` say what
|
|
117
|
+
# to do).
|
|
39
118
|
#
|
|
40
119
|
# The front-door session (ai.maestro.<first>-session) is NEVER restarted or
|
|
41
120
|
# killed here: after a HEALTHY upgrade this writes
|
|
42
|
-
# state/session/upgrade-notice.json {from,to,at}; the session's feed
|
|
43
|
-
# it as an `upgrade-available` directive
|
|
44
|
-
# idle moment via `maestro session
|
|
45
|
-
#
|
|
121
|
+
# state/session/upgrade-notice.json {from,to,at}; the session's feed
|
|
122
|
+
# (lib/session/feed-core.mjs) surfaces it as an `upgrade-available` directive
|
|
123
|
+
# and the session restarts itself at an idle moment via `maestro session
|
|
124
|
+
# restart` (the supervisor relaunches on the new code). The same file with
|
|
125
|
+
# {healthy:false, reason} is a REPORT — the feed emits `daemon-unhealthy`
|
|
126
|
+
# for it, a distinct action, so no session restarts on a health report. A
|
|
127
|
+
# rollback leaves no notice. Every attempt also writes
|
|
46
128
|
# state/autoupdate/last.json {from,to,at,ok,healthy,reason}, which the beat
|
|
47
129
|
# reports as machine.upgrade. See
|
|
48
130
|
# docs/superpowers/specs/2026-09-08-front-door-session-design.md §3.10 + WP-M6.
|
|
@@ -53,14 +135,18 @@
|
|
|
53
135
|
#
|
|
54
136
|
# Test knobs (scripts/local-triggers/autoupdate.test.mjs): MAESTRO_AUTOUPDATE_JITTER_MAX
|
|
55
137
|
# (default 600) and MAESTRO_AUTOUPDATE_HEALTH_WAIT (default 30), both seconds;
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
138
|
+
# MAESTRO_AUTOUPDATE_STABLE_S (default 90; MAESTRO_AUTOUPDATE_STABLE_GAP_S is the
|
|
139
|
+
# sleep between the two pid samples, default = STABLE_S, zeroed by the test) and
|
|
140
|
+
# MAESTRO_AUTOUPDATE_BEAT_FRESH_S (default 300); MAESTRO_AUTOUPDATE_RETRY_SLEEP
|
|
141
|
+
# (default 20 s, ×attempt);
|
|
142
|
+
# MAESTRO_AUTOUPDATE_LOCK_STALE_S (default 7200); MAESTRO_AUTOUPDATE_FAILED_HOLD_S
|
|
143
|
+
# (default 86400); MAESTRO_AUTOUPDATE_PATH_PREFIX (a stub-binary dir that wins
|
|
144
|
+
# over PATH — the test stubs npm, launchctl, pgrep AND ps there);
|
|
59
145
|
# MAESTRO_SKIP_GLOBAL_INSTALL=1 (never touch the global install).
|
|
60
146
|
# =============================================================================
|
|
61
147
|
set -uo pipefail
|
|
62
148
|
# MAESTRO_AUTOUPDATE_PATH_PREFIX: a dir that wins over the standard prepend
|
|
63
|
-
# (the test puts its npm/launchctl/pgrep stubs there so a run never reaches
|
|
149
|
+
# (the test puts its npm/launchctl/pgrep/ps stubs there so a run never reaches
|
|
64
150
|
# the real registry or launchd).
|
|
65
151
|
export PATH="${MAESTRO_AUTOUPDATE_PATH_PREFIX:+$MAESTRO_AUTOUPDATE_PATH_PREFIX:}/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${PATH:-}"
|
|
66
152
|
|
|
@@ -68,6 +154,11 @@ export PATH="${MAESTRO_AUTOUPDATE_PATH_PREFIX:+$MAESTRO_AUTOUPDATE_PATH_PREFIX:}
|
|
|
68
154
|
# The copy removes itself on exit (REEXEC_COPY is only ever a mktemp'd
|
|
69
155
|
# maestro-autoupdate.* file — never the agent dir's script).
|
|
70
156
|
if [ "${MAESTRO_AUTOUPDATE_REEXEC:-}" != "1" ]; then
|
|
157
|
+
# Pin the agent dir BEFORE re-exec'ing: the copy lives in $TMPDIR, so its own
|
|
158
|
+
# location says nothing about the agent. launchd's plist sets AGENT_ROOT; a
|
|
159
|
+
# hand-run `bash scripts/local-triggers/autoupdate.sh` did not, resolved
|
|
160
|
+
# AGENT_DIR to $TMPDIR/../.., and exited 0 having done nothing.
|
|
161
|
+
export AGENT_ROOT="${AGENT_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
|
71
162
|
_tmp="$(mktemp "${TMPDIR:-/tmp}/maestro-autoupdate.XXXXXX")" || exit 0
|
|
72
163
|
cp "${BASH_SOURCE[0]}" "$_tmp" 2>/dev/null || exit 0
|
|
73
164
|
MAESTRO_AUTOUPDATE_REEXEC=1 exec /bin/bash "$_tmp" "$@"
|
|
@@ -144,46 +235,150 @@ npm_view_latest(){ # prints the latest version, or nothing after RETRIES attempt
|
|
|
144
235
|
done
|
|
145
236
|
}
|
|
146
237
|
|
|
147
|
-
# ──
|
|
148
|
-
|
|
149
|
-
LATEST="$(npm_view_latest)" || LATEST=""
|
|
150
|
-
[ -z "$LATEST" ] && { log "npm view failed after $RETRIES attempts; skip (still $CUR)"; exit 0; }
|
|
151
|
-
if [ "$CUR" = "$LATEST" ] || [ "$(printf '%s\n%s\n' "$CUR" "$LATEST" | sort -V | tail -1)" != "$LATEST" ]; then
|
|
152
|
-
log "up to date ($CUR)"; exit 0
|
|
153
|
-
fi
|
|
154
|
-
|
|
155
|
-
# ── failed-target hold ───────────────────────────────────────────────────────
|
|
156
|
-
# last.json is the memory of the health gate: a LATEST that was installed here
|
|
157
|
-
# and rolled back (or could not even be rolled back) is not retried every hour
|
|
158
|
-
# — that would be two installs, two global refreshes and two daemon restarts an
|
|
159
|
-
# hour until someone touched the kill-switch. An install-failed (network) entry
|
|
160
|
-
# does not hold. Fail-open: an unreadable last.json holds nothing.
|
|
161
|
-
FAILED_HOLD_S="${MAESTRO_AUTOUPDATE_FAILED_HOLD_S:-86400}"
|
|
162
|
-
failed_hold_reason(){ # prints "<reason> at <at>" when LATEST failed health within FAILED_HOLD_S
|
|
163
|
-
node -e '
|
|
164
|
-
const [file, latest, holdS] = process.argv.slice(1);
|
|
165
|
-
try {
|
|
166
|
-
const j = JSON.parse(require("fs").readFileSync(file, "utf8"));
|
|
167
|
-
const at = Date.parse(j.at);
|
|
168
|
-
if (j.to === latest && j.ok === false && /unhealthy/.test(String(j.reason)) && Number.isFinite(at) && Date.now() - at < Number(holdS) * 1000) process.stdout.write(`${j.reason} at ${j.at}`);
|
|
169
|
-
} catch {}
|
|
170
|
-
' "$AGENT_DIR/state/autoupdate/last.json" "$LATEST" "$FAILED_HOLD_S" 2>/dev/null
|
|
171
|
-
}
|
|
172
|
-
HOLD="$(failed_hold_reason)"
|
|
173
|
-
if [ -n "$HOLD" ]; then
|
|
174
|
-
log "SKIP: $LATEST already failed the health gate here ($HOLD); holding ${FAILED_HOLD_S}s (still $CUR) — rm state/autoupdate/last.json to retry now"
|
|
175
|
-
exit 0
|
|
176
|
-
fi
|
|
177
|
-
log "UPDATE $CUR -> $LATEST"
|
|
178
|
-
|
|
238
|
+
# ── this agent, its daemon, and the health gate ──────────────────────────────
|
|
239
|
+
# Defined BEFORE the version check: the up-to-date path runs the gate too (d).
|
|
179
240
|
UID_="$(id -u)"
|
|
180
241
|
AGENT_FIRST="$(node -p "String(require('$AGENT_DIR/config/agent.json').firstName || '').toLowerCase()" 2>/dev/null)"
|
|
181
242
|
[ -n "$AGENT_FIRST" ] && log "agent: $AGENT_FIRST (launchd/pgrep questions are scoped to ai.maestro.$AGENT_FIRST-*)"
|
|
182
|
-
|
|
243
|
+
DAEMON_PATTERN="$AGENT_DIR/scripts/daemon/maestro-daemon.mjs"
|
|
244
|
+
# The daemon's argv is exactly `<node> <AGENT_DIR>/scripts/daemon/maestro-daemon.mjs`
|
|
245
|
+
# (launchd-wrapper.sh execs it that way), so the pgrep pattern is anchored to
|
|
246
|
+
# that whole argument list. `pgrep -f <path>` alone matches ANY process whose
|
|
247
|
+
# argv merely contains the path — an editor, a grep, a `node --inspect` by an
|
|
248
|
+
# operator — and one of those appearing between the two pid samples reads as
|
|
249
|
+
# a pid-set change, i.e. "crash loop".
|
|
250
|
+
DAEMON_ARGV_RE="^[^ ]*/node $(printf '%s' "$DAEMON_PATTERN" | sed 's/[][\.*^$]/\\&/g')\$"
|
|
251
|
+
DLOG_DIR="$AGENT_DIR/logs/daemon"
|
|
183
252
|
HEALTH_WAIT="${MAESTRO_AUTOUPDATE_HEALTH_WAIT:-30}"
|
|
253
|
+
STABLE_S="${MAESTRO_AUTOUPDATE_STABLE_S:-90}"
|
|
254
|
+
STABLE_GAP_S="${MAESTRO_AUTOUPDATE_STABLE_GAP_S:-$STABLE_S}" # the gap between the two pid samples; the test zeroes it (its stub changes per CALL)
|
|
255
|
+
BEAT_FRESH_S="${MAESTRO_AUTOUPDATE_BEAT_FRESH_S:-300}"
|
|
256
|
+
LAST_BEAT="$AGENT_DIR/state/org/last-beat.json"
|
|
257
|
+
MESH_FILE="$AGENT_DIR/lib/org/mesh.mjs"
|
|
258
|
+
mesh_records_beats(){ grep -q "LAST_BEAT_FILE" "$MESH_FILE" 2>/dev/null; } # the installed mesh writes state/org/last-beat.json (the release that added this gate)
|
|
259
|
+
# The lines the daemon writes on its way down. Tight on purpose: the daemon log
|
|
260
|
+
# is free text (LLM output included), so every pattern here is a prefix the
|
|
261
|
+
# code emits — lib/util/unhandled.mjs via maestro-daemon.mjs's onEvent, the
|
|
262
|
+
# main() catch in agent-daemon.mjs, launchd-wrapper.sh, and Node's own code
|
|
263
|
+
# for an import that cannot resolve (how a bad release most often dies).
|
|
264
|
+
FATAL_RE='\[DAEMON\] uncaughtException:|\[daemon\] Fatal:|\[wrapper\] FATAL:|ERR_MODULE_NOT_FOUND'
|
|
265
|
+
HEALTH_REASON="" # set by is_healthy on failure; a phrase, no quotes
|
|
184
266
|
DAEMON_LABEL=""
|
|
185
267
|
SESSION_LABEL=""
|
|
186
268
|
|
|
269
|
+
daemon_pids(){ # every pid whose argv IS this agent's daemon entry (see DAEMON_ARGV_RE), sorted, space-joined ("" when none)
|
|
270
|
+
pgrep -f "$DAEMON_ARGV_RE" 2>/dev/null | sort -n | tr '\n' ' ' | sed 's/ *$//'
|
|
271
|
+
}
|
|
272
|
+
etime_seconds(){ # $1 = ps etime ([[dd-]hh:]mm:ss) → seconds; "" when unparseable
|
|
273
|
+
local s="$1" d=0 h=0 m=0 sec=0 a b c
|
|
274
|
+
[ -n "$s" ] || return 0
|
|
275
|
+
case "$s" in *-*) d="${s%%-*}"; s="${s#*-}" ;; esac
|
|
276
|
+
IFS=: read -r a b c <<< "$s"
|
|
277
|
+
if [ -n "${c:-}" ]; then h="$a"; m="$b"; sec="$c"; else m="$a"; sec="${b:-0}"; fi
|
|
278
|
+
case "$d$h$m$sec" in *[!0-9]*) return 0 ;; esac
|
|
279
|
+
echo $(( 10#$d * 86400 + 10#$h * 3600 + 10#$m * 60 + 10#$sec ))
|
|
280
|
+
}
|
|
281
|
+
pid_uptime_s(){ # $1 = pid → seconds since it started, "" when ps cannot say
|
|
282
|
+
etime_seconds "$(ps -o etime= -p "$1" 2>/dev/null | tr -d ' ')"
|
|
283
|
+
}
|
|
284
|
+
day_of_epoch(){ # $1 = epoch seconds → YYYY-MM-DD in local time (the wrapper's `date +%Y-%m-%d` basis)
|
|
285
|
+
date -r "$1" +%Y-%m-%d 2>/dev/null || date -d "@$1" +%Y-%m-%d 2>/dev/null
|
|
286
|
+
}
|
|
287
|
+
daemon_log_for(){ # $1 = uptime seconds → the log file the wrapper opened for THIS incarnation (named for its start day)
|
|
288
|
+
echo "$DLOG_DIR/daemon-$(day_of_epoch $(( $(date +%s) - $1 ))).log"
|
|
289
|
+
}
|
|
290
|
+
first_fatal_after(){ # $1 = log file, $2 = line offset (lines up to it predate this daemon) → the first fatal line, or nothing
|
|
291
|
+
[ -f "$1" ] || return 0
|
|
292
|
+
tail -n +"$(( $2 + 1 ))" "$1" 2>/dev/null | grep -m1 -E "$FATAL_RE"
|
|
293
|
+
return 0
|
|
294
|
+
}
|
|
295
|
+
# The daemon log is APPENDED to (launchd-wrapper.sh `>>`) and shared by every
|
|
296
|
+
# incarnation that starts on the same day, so "since this daemon" needs a
|
|
297
|
+
# marker. maestro-daemon.mjs prints `[DAEMON] boot pid=<pid> at=<iso>` as its
|
|
298
|
+
# first line (since the release that added this gate); for a daemon older than that the LAST generic boot line
|
|
299
|
+
# — "[daemon] Running" / "[DAEMON] org-mesh connected", which only a process
|
|
300
|
+
# that got through boot prints — is the best available split. Without either,
|
|
301
|
+
# a one-off crash at 09:00 and the healthy respawn at 09:01 share a file and
|
|
302
|
+
# the crash read as "fatal since restart" every hour until midnight.
|
|
303
|
+
BOOT_RE='\[daemon\] Running|\[DAEMON\] org-mesh connected'
|
|
304
|
+
boot_offset(){ # $1 = log file, $2 = pid → the line count that precedes THIS incarnation's boot marker (0 when there is none)
|
|
305
|
+
local n
|
|
306
|
+
[ -f "$1" ] || { echo 0; return 0; }
|
|
307
|
+
n="$(grep -n -F "[DAEMON] boot pid=$2 " "$1" 2>/dev/null | tail -1 | cut -d: -f1)"
|
|
308
|
+
[ -n "$n" ] || n="$(grep -n -E "$BOOT_RE" "$1" 2>/dev/null | tail -1 | cut -d: -f1)"
|
|
309
|
+
echo $(( ${n:-1} - 1 ))
|
|
310
|
+
}
|
|
311
|
+
org_enrolled(){ # the SAME answer the daemon gives: lib/org/client.mjs loadOrgConfig + isEnabled — org.cohort.enabled AND a base (or apiUrl+orgId). A seat that fails that test never beats, so the gate does not ask it to.
|
|
312
|
+
node --input-type=module -e '
|
|
313
|
+
const [root] = process.argv.slice(1);
|
|
314
|
+
const { loadOrgConfig, isEnabled } = await import(root + "/lib/org/client.mjs");
|
|
315
|
+
process.exit(isEnabled(loadOrgConfig(root)) ? 0 : 3);
|
|
316
|
+
' "$AGENT_DIR" 2>/dev/null
|
|
317
|
+
case $? in 0) return 0 ;; 3) return 1 ;; esac
|
|
318
|
+
log "WARN: lib/org/client.mjs could not answer whether this seat is enrolled — treating it as not enrolled (the daemon would too)"
|
|
319
|
+
return 1
|
|
320
|
+
}
|
|
321
|
+
beat_not_acknowledged(){ # $1 = epoch seconds the daemon started, $2 = installed version → prints WHY the org has not acknowledged this daemon ON THIS VERSION, or nothing when it has
|
|
322
|
+
node -e '
|
|
323
|
+
const [file, sinceS, freshS, meshRecords, installed] = process.argv.slice(1);
|
|
324
|
+
const out = (s) => process.stdout.write(s);
|
|
325
|
+
let j;
|
|
326
|
+
try { j = JSON.parse(require("fs").readFileSync(file, "utf8")); }
|
|
327
|
+
catch (e) { out(`no beat recorded (${e && e.code === "ENOENT" ? (meshRecords === "1" ? "state/org/last-beat.json absent — this daemon process has never completed a beat on this code" : "state/org/last-beat.json absent, and the installed lib/org/mesh.mjs does not record beats at all — a preserved local copy older than this gate? maestro upgrade --verify") : "state/org/last-beat.json unreadable"})`); process.exit(0); }
|
|
328
|
+
const at = Date.parse(j && j.at);
|
|
329
|
+
if (!Number.isFinite(at)) { out("last-beat.json carries no parseable at"); process.exit(0); }
|
|
330
|
+
const ageS = Math.round((Date.now() - at) / 1000);
|
|
331
|
+
if (j.ok !== true) out(`last beat NOT accepted by the org (${j.code || "?"}${j.message ? ": " + String(j.message).replace(/["\\\\]/g, "") : ""}) at ${j.at}`);
|
|
332
|
+
else if (at < Number(sinceS) * 1000) out(`last accepted beat (${j.at}) predates this daemon process`);
|
|
333
|
+
else if (ageS > Number(freshS)) out(`last accepted beat is ${ageS}s old (> ${freshS}s; the org already calls this seat offline)`);
|
|
334
|
+
else if (installed && typeof j.sdkVersion === "string" && j.sdkVersion && j.sdkVersion !== installed) out(`last accepted beat (${j.at}) told the org sdkVersion ${j.sdkVersion}, but ${installed} is installed — the org sees the old version`);
|
|
335
|
+
' "$LAST_BEAT" "$1" "$BEAT_FRESH_S" "$(mesh_records_beats && echo 1 || echo 0)" "${2:-}" 2>/dev/null
|
|
336
|
+
}
|
|
337
|
+
# The daemon log offset "since the restart" — captured right before a kickstart
|
|
338
|
+
# (see restart_daemon). is_healthy takes the LATER of this and the boot marker
|
|
339
|
+
# (boot_offset), so with no restart the scan still starts at the running
|
|
340
|
+
# daemon's own boot line, never at the top of a shared start-day file.
|
|
341
|
+
PRE_LOG=""; PRE_LOG_LINES=0
|
|
342
|
+
installed_version(){ # the version in node_modules RIGHT NOW — re-read per call, because is_healthy runs after an install and again after a rollback
|
|
343
|
+
node -p "require('$AGENT_DIR/node_modules/$PKG/package.json').version" 2>/dev/null || true
|
|
344
|
+
}
|
|
345
|
+
sibling_job_audit(){ # log-only: every OTHER ai.maestro.<first>-* job whose LAST exit was non-zero. Never a gate reason — a broken relay poller is not a reason to roll the SDK back — but the daemon log is not where those jobs fail, and nothing else reads `launchctl list` for them.
|
|
346
|
+
[ -n "${AGENT_FIRST:-}" ] || return 0
|
|
347
|
+
local bad
|
|
348
|
+
# The -session job's 75 is EX_TEMPFAIL: the supervisor's DESIGNED "the session ended, relaunch me" exit (scripts/session/supervisor.mjs), the signature of every clean relaunch — not a failure.
|
|
349
|
+
bad="$(launchctl list 2>/dev/null | awk -v p="ai.maestro.${AGENT_FIRST}-" -v sess="ai.maestro.${AGENT_FIRST}-session" 'NR>1 && index($3, p)==1 && $2 != "0" && $2 != "-" && !($3 == sess && $2 == "75") { printf "%s%s(%s)", (n++ ? ", " : ""), $3, $2 }')"
|
|
350
|
+
[ -n "$bad" ] && log "sibling jobs with a non-zero last exit (not a gate reason; see logs/launchd and logs/polling): $bad"
|
|
351
|
+
return 0
|
|
352
|
+
}
|
|
353
|
+
is_healthy(){ # (a)+(b)+(c) above. Sets HEALTH_REASON on failure.
|
|
354
|
+
local p1 p2 pid up oldest oldest_pid dlog fatal why offset boot
|
|
355
|
+
HEALTH_REASON=""
|
|
356
|
+
p1="$(daemon_pids)"
|
|
357
|
+
[ -n "$p1" ] || { HEALTH_REASON="no daemon process for $DAEMON_PATTERN"; return 1; }
|
|
358
|
+
[ "$STABLE_GAP_S" -gt 0 ] && sleep "$STABLE_GAP_S"
|
|
359
|
+
p2="$(daemon_pids)"
|
|
360
|
+
[ "$p1" = "$p2" ] || { HEALTH_REASON="daemon pid changed within ${STABLE_GAP_S}s (${p1} -> ${p2:-none}) — crash loop"; return 1; }
|
|
361
|
+
oldest=""
|
|
362
|
+
for pid in $p1; do
|
|
363
|
+
up="$(pid_uptime_s "$pid")"
|
|
364
|
+
[ -n "$up" ] && [ "$up" -ge "$STABLE_S" ] || { HEALTH_REASON="daemon pid $pid uptime ${up:-?}s < ${STABLE_S}s — crash loop"; return 1; }
|
|
365
|
+
[ -n "$oldest" ] && [ "$oldest" -ge "$up" ] || { oldest="$up"; oldest_pid="$pid"; }
|
|
366
|
+
done
|
|
367
|
+
dlog="$(daemon_log_for "$oldest")"
|
|
368
|
+
offset=0; [ "$dlog" = "$PRE_LOG" ] && offset="$PRE_LOG_LINES"
|
|
369
|
+
boot="$(boot_offset "$dlog" "$oldest_pid")"
|
|
370
|
+
[ "$boot" -gt "$offset" ] && offset="$boot"
|
|
371
|
+
fatal="$(first_fatal_after "$dlog" "$offset")"
|
|
372
|
+
[ -z "$fatal" ] || { HEALTH_REASON="fatal in $(basename "$dlog") since this daemon booted (scanned from line $(( offset + 1 ))): $(printf '%s' "$fatal" | tr -d '"\\' | cut -c1-160)"; return 1; }
|
|
373
|
+
if org_enrolled; then
|
|
374
|
+
why="$(beat_not_acknowledged $(( $(date +%s) - oldest - 2 )) "$(installed_version)")"
|
|
375
|
+
[ -z "$why" ] || { HEALTH_REASON="$why"; return 1; }
|
|
376
|
+
else
|
|
377
|
+
log "org not enrolled in config/org.yaml — beat acknowledgement not required"
|
|
378
|
+
fi
|
|
379
|
+
return 0
|
|
380
|
+
}
|
|
381
|
+
|
|
187
382
|
first_plist(){ # $1 = suffix, $2 = dir — this agent's plist for that suffix, or nothing (unquoted so `*` globs when no name is known)
|
|
188
383
|
ls "$2"/ai.maestro.${AGENT_FIRST:-*}-"$1".plist 2>/dev/null | head -1
|
|
189
384
|
}
|
|
@@ -208,9 +403,212 @@ label_loaded(){ # $1 = label — measured in the gui domain (right from ssh too)
|
|
|
208
403
|
}
|
|
209
404
|
restart_daemon(){
|
|
210
405
|
resolve_labels
|
|
406
|
+
# Mark "since the restart" for the fatal scan: the new process appends to
|
|
407
|
+
# today's file (the wrapper names it for its start day), after these lines.
|
|
408
|
+
PRE_LOG="$DLOG_DIR/daemon-$(date +%Y-%m-%d).log"
|
|
409
|
+
PRE_LOG_LINES=0
|
|
410
|
+
[ -f "$PRE_LOG" ] && PRE_LOG_LINES="$(wc -l < "$PRE_LOG" 2>/dev/null | tr -d ' ')"
|
|
411
|
+
PRE_LOG_LINES="${PRE_LOG_LINES:-0}"
|
|
211
412
|
if [ -z "$DAEMON_LABEL" ]; then log "no -daemon plist under ~/Library/LaunchAgents — nothing to kickstart (maestro upgrade installs it; doctor will say)"; return 0; fi
|
|
212
413
|
launchctl kickstart -k "gui/$UID_/$DAEMON_LABEL" >> "$LOG" 2>&1 || log "WARN: kickstart $DAEMON_LABEL nonzero"
|
|
213
414
|
}
|
|
415
|
+
session_reconciled(){ # only asked when a -session plist was generated; never a rollback reason
|
|
416
|
+
label_loaded "$SESSION_LABEL" || { log "reconcile-failed: session job $SESSION_LABEL is not loaded (maestro session start)"; return 1; }
|
|
417
|
+
pgrep -f "$AGENT_DIR/scripts/session/supervisor" >/dev/null 2>&1 || { log "reconcile-failed: $SESSION_LABEL is loaded but no supervisor process is alive for $AGENT_DIR (maestro session status)"; return 1; }
|
|
418
|
+
return 0
|
|
419
|
+
}
|
|
420
|
+
json_str(){ printf '%s' "$1" | tr -d '"\\' | tr '\n' ' '; } # a value safe inside "…" in the JSON files below
|
|
421
|
+
write_notice(){ # $1 = from, $2 = to, [$3 = reason → healthy:false] — tell the front-door session; never restart it here
|
|
422
|
+
local dir="$AGENT_DIR/state/session" tmp
|
|
423
|
+
mkdir -p "$dir" 2>/dev/null || return 0
|
|
424
|
+
tmp="$(mktemp "$dir/.upgrade-notice.XXXXXX" 2>/dev/null)" || return 0
|
|
425
|
+
if [ -n "${3:-}" ]; then
|
|
426
|
+
printf '{\n "from": "%s",\n "to": "%s",\n "at": "%s",\n "healthy": false,\n "reason": "%s"\n}\n' "$1" "$2" "$(date -u +%FT%TZ)" "$(json_str "$3")" > "$tmp" \
|
|
427
|
+
&& mv -f "$tmp" "$dir/upgrade-notice.json" \
|
|
428
|
+
&& log "session notified: upgrade-notice.json healthy:false on $2 (no restart — the session surfaces it)"
|
|
429
|
+
else
|
|
430
|
+
printf '{\n "from": "%s",\n "to": "%s",\n "at": "%s"\n}\n' "$1" "$2" "$(date -u +%FT%TZ)" > "$tmp" \
|
|
431
|
+
&& mv -f "$tmp" "$dir/upgrade-notice.json" \
|
|
432
|
+
&& log "session notified: upgrade-notice.json $1 -> $2 (session restarts itself when idle)"
|
|
433
|
+
fi
|
|
434
|
+
rm -f "$tmp" 2>/dev/null
|
|
435
|
+
return 0
|
|
436
|
+
}
|
|
437
|
+
write_upgrade_notice(){ write_notice "$1" "$2"; }
|
|
438
|
+
LAST_JSON="$AGENT_DIR/state/autoupdate/last.json"
|
|
439
|
+
write_last(){ # $1 from, $2 to, $3 ok, $4 healthy, $5 reason — state/autoupdate/last.json (→ beat machine.upgrade)
|
|
440
|
+
local dir="$AGENT_DIR/state/autoupdate" tmp
|
|
441
|
+
mkdir -p "$dir" 2>/dev/null || return 0
|
|
442
|
+
tmp="$(mktemp "$dir/.last.XXXXXX" 2>/dev/null)" || return 0
|
|
443
|
+
printf '{\n "from": "%s",\n "to": "%s",\n "at": "%s",\n "ok": %s,\n "healthy": %s,\n "reason": "%s"\n}\n' \
|
|
444
|
+
"$1" "$2" "$(date -u +%FT%TZ)" "$3" "$4" "$(json_str "$5")" > "$tmp" && mv -f "$tmp" "$LAST_JSON"
|
|
445
|
+
rm -f "$tmp" 2>/dev/null
|
|
446
|
+
return 0
|
|
447
|
+
}
|
|
448
|
+
last_reason(){ # the reason field of last.json, or nothing
|
|
449
|
+
node -e 'try { process.stdout.write(String(JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).reason || "")); } catch {}' "$LAST_JSON" 2>/dev/null
|
|
450
|
+
}
|
|
451
|
+
# The version the RUNNING daemon started on. scripts/daemon/health.mjs resolves
|
|
452
|
+
# sdk_version once at load and writes it to state/dashboards/daemon-health.yaml
|
|
453
|
+
# with its pid, precisely so a reader can tell the installed package from the
|
|
454
|
+
# code a live process is executing. Printed only when the dashboard's pid is
|
|
455
|
+
# one of the sampled daemon pids — a dashboard left by a dead process says
|
|
456
|
+
# nothing about this one.
|
|
457
|
+
DAEMON_HEALTH="$AGENT_DIR/state/dashboards/daemon-health.yaml"
|
|
458
|
+
running_daemon_version(){ # → the sdk_version of the LIVE daemon (dashboard pid ∈ daemon_pids), or nothing
|
|
459
|
+
local v pid
|
|
460
|
+
[ -f "$DAEMON_HEALTH" ] || return 0
|
|
461
|
+
v="$(sed -nE 's/^sdk_version:[[:space:]]*([^[:space:]]+).*$/\1/p' "$DAEMON_HEALTH" 2>/dev/null | head -1)"
|
|
462
|
+
pid="$(sed -nE 's/^pid:[[:space:]]*([0-9]+).*$/\1/p' "$DAEMON_HEALTH" 2>/dev/null | head -1)"
|
|
463
|
+
[ -n "$v" ] && [ -n "$pid" ] || return 0
|
|
464
|
+
case " $(daemon_pids) " in *" $pid "*) printf '%s' "$v" ;; esac
|
|
465
|
+
}
|
|
466
|
+
daemon_start_epoch(){ # of the longest-running daemon pid, "" when none
|
|
467
|
+
local p up best=""
|
|
468
|
+
for p in $(daemon_pids); do up="$(pid_uptime_s "$p")"; [ -n "$up" ] || continue; [ -n "$best" ] && [ "$best" -ge "$up" ] || best="$up"; done
|
|
469
|
+
[ -n "$best" ] && echo $(( $(date +%s) - best ))
|
|
470
|
+
return 0
|
|
471
|
+
}
|
|
472
|
+
daemon_predates_mesh(){ # no beat record, and the live daemon started BEFORE the installed lib/org/mesh.mjs was written → it cannot be running that code
|
|
473
|
+
local start mt
|
|
474
|
+
[ -f "$LAST_BEAT" ] && return 1
|
|
475
|
+
mesh_records_beats || return 1
|
|
476
|
+
start="$(daemon_start_epoch)"; [ -n "$start" ] || return 1
|
|
477
|
+
mt="$(stat -f %m "$MESH_FILE" 2>/dev/null || stat -c %Y "$MESH_FILE" 2>/dev/null)"; [ -n "$mt" ] || return 1
|
|
478
|
+
[ "$start" -lt "$mt" ]
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
# ── version check ────────────────────────────────────────────────────────────
|
|
482
|
+
CUR="$(node -p "require('$AGENT_DIR/node_modules/$PKG/package.json').version" 2>/dev/null || echo 0.0.0)"
|
|
483
|
+
LATEST="$(npm_view_latest)" || LATEST=""
|
|
484
|
+
[ -z "$LATEST" ] && { log "npm view failed after $RETRIES attempts; skip (still $CUR)"; exit 0; }
|
|
485
|
+
# (f) AHEAD OF THE REGISTRY is not "up to date", and the difference is the whole
|
|
486
|
+
# fleet. The branch below treats `CUR >= LATEST` as one outcome, so a seat
|
|
487
|
+
# carrying a build that was never published logs the same reassuring
|
|
488
|
+
# "up to date (<CUR>)" as a seat that is genuinely current — which is how the
|
|
489
|
+
# health gate written for the six-day crash loop came to be running on ZERO
|
|
490
|
+
# machines: the authoring seat installed 2.18.0 from its own tree, npm's latest
|
|
491
|
+
# stopped at 2.17.0, and every hourly run on every box said "up to date". A
|
|
492
|
+
# gate that lives only on the machine that wrote it is live nowhere, and
|
|
493
|
+
# nothing said so. Say it, in the log AND in last.json — which the beat carries
|
|
494
|
+
# to the org as machine.upgrade, so the fleet holds the fact rather than the
|
|
495
|
+
# one box that already has the code.
|
|
496
|
+
AHEAD=""
|
|
497
|
+
if [ "$CUR" != "$LATEST" ] && [ "$(printf '%s\n%s\n' "$CUR" "$LATEST" | sort -V | tail -1)" = "$CUR" ]; then
|
|
498
|
+
AHEAD=1
|
|
499
|
+
log "AHEAD OF REGISTRY: installed $CUR, npm latest is $LATEST — this build was never published. Every OTHER seat follows @latest and is pinned at $LATEST, so nothing in $CUR has reached the fleet. Publish it (npm publish) or roll this seat back; do not read the line below as the fleet being current."
|
|
500
|
+
fi
|
|
501
|
+
if [ "$CUR" = "$LATEST" ] || [ "$(printf '%s\n%s\n' "$CUR" "$LATEST" | sort -V | tail -1)" != "$LATEST" ]; then
|
|
502
|
+
# (d) up to date is not the same as well. Run the gate on the CURRENT
|
|
503
|
+
# version; there is nothing to roll back, so the outcome is evidence —
|
|
504
|
+
# loud in this log, in last.json (→ the beat's machine.upgrade), and in the
|
|
505
|
+
# session's feed on the transition into an unhealthy episode.
|
|
506
|
+
log "up to date ($CUR) — checking daemon health: pid stability ${STABLE_S}s, fatal scan, org-acknowledged beat (< ${BEAT_FRESH_S}s)"
|
|
507
|
+
PREV_REASON="$(last_reason)"
|
|
508
|
+
# (e) A daemon still running an OLDER version than the one installed. That
|
|
509
|
+
# is what a hand-run `maestro upgrade` leaves behind: its post-steps never
|
|
510
|
+
# restart the daemon (lib/upgrade/post-steps.mjs — "the daemon is restarted
|
|
511
|
+
# by autoupdate, never here") and its verify row says the new version "takes
|
|
512
|
+
# effect when autoupdate kickstarts it". Until this branch existed that was
|
|
513
|
+
# a promise nobody kept: the installed version was already @latest, so this
|
|
514
|
+
# path never kickstarted anything, the daemon ran the old code until the
|
|
515
|
+
# NEXT release, and — because an earlier mesh.mjs never wrote
|
|
516
|
+
# state/org/last-beat.json — the gate below read the missing record as
|
|
517
|
+
# "no beat recorded" and declared a stable, beating daemon unhealthy. The
|
|
518
|
+
# kickstart is the upgrade's missing last step; the gate then runs as after
|
|
519
|
+
# any restart, and a healthy outcome completes the hop the same way an
|
|
520
|
+
# automatic one does (notice → the session restarts itself when idle).
|
|
521
|
+
# Two signals, either is enough: the dashboard's sdk_version differs from
|
|
522
|
+
# the installed one (a version hop), or — same version — there is no beat
|
|
523
|
+
# record and the daemon started before the installed lib/org/mesh.mjs was
|
|
524
|
+
# written (a file change without a version bump: it cannot be running that
|
|
525
|
+
# code, and that code is the only thing that writes the record).
|
|
526
|
+
RUNNING_V="$(running_daemon_version)"
|
|
527
|
+
STALE_FROM=""; STALE_HOP=""
|
|
528
|
+
if [ -n "$RUNNING_V" ] && [ "$RUNNING_V" != "$CUR" ]; then
|
|
529
|
+
STALE_FROM="$RUNNING_V"; STALE_HOP=1
|
|
530
|
+
log "stale daemon: running $RUNNING_V, installed $CUR — a hand-run maestro upgrade left the daemon restart to this job; kickstarting"
|
|
531
|
+
elif daemon_predates_mesh; then
|
|
532
|
+
STALE_FROM="${RUNNING_V:-$CUR}"
|
|
533
|
+
log "stale daemon: started $(daemon_start_epoch | xargs -I{} date -u -r {} +%FT%TZ 2>/dev/null) — before the installed lib/org/mesh.mjs was written — so it runs code that never records a beat; kickstarting onto the installed code"
|
|
534
|
+
fi
|
|
535
|
+
if [ -n "$STALE_FROM" ]; then
|
|
536
|
+
restart_daemon
|
|
537
|
+
[ "$HEALTH_WAIT" -gt 0 ] && sleep "$HEALTH_WAIT"
|
|
538
|
+
fi
|
|
539
|
+
if is_healthy; then
|
|
540
|
+
log "healthy on $CUR"
|
|
541
|
+
sibling_job_audit
|
|
542
|
+
if [ -n "$STALE_HOP" ]; then
|
|
543
|
+
# A real version hop: complete it the way an automatic one completes —
|
|
544
|
+
# the notice tells the session to restart itself onto the new code.
|
|
545
|
+
log "OK: daemon now on $CUR (was $STALE_FROM) — the manual upgrade is complete"
|
|
546
|
+
write_upgrade_notice "$STALE_FROM" "$CUR"
|
|
547
|
+
write_last "$STALE_FROM" "$CUR" true true ""
|
|
548
|
+
elif [ -n "$STALE_FROM" ]; then
|
|
549
|
+
# Same version, newer files: the daemon is reconciled; the session is
|
|
550
|
+
# not told to restart (a touched or restored file must not cost it its
|
|
551
|
+
# context — the version did not change).
|
|
552
|
+
log "OK: daemon restarted onto the installed $CUR code; no session notice (same version)"
|
|
553
|
+
write_last "$CUR" "$CUR" true true ""
|
|
554
|
+
elif [ -n "$AHEAD" ]; then
|
|
555
|
+
# The DAEMON is well; the FLEET is not. Recorded as ok/healthy — because
|
|
556
|
+
# it is — with the finding in `reason`, so the org learns that this seat
|
|
557
|
+
# executes code no other seat can install. Written every run, not once
|
|
558
|
+
# per episode: unlike an unhealthy daemon this does not clear itself, and
|
|
559
|
+
# a fact that ages out of one log file is how six days went by.
|
|
560
|
+
log "OK: healthy on $CUR, but AHEAD of the registry ($LATEST) — recorded for the org as ahead-of-registry"
|
|
561
|
+
write_last "$CUR" "$CUR" true true "ahead-of-registry: npm latest is $LATEST"
|
|
562
|
+
else
|
|
563
|
+
case "$PREV_REASON" in unhealthy-current*)
|
|
564
|
+
log "recovered: last.json no longer reports unhealthy-current"
|
|
565
|
+
write_last "$CUR" "$CUR" true true "" ;;
|
|
566
|
+
ahead-of-registry*)
|
|
567
|
+
log "recovered: $CUR is on the registry now (latest $LATEST) — last.json no longer reports ahead-of-registry"
|
|
568
|
+
write_last "$CUR" "$CUR" true true "" ;;
|
|
569
|
+
esac
|
|
570
|
+
fi
|
|
571
|
+
else
|
|
572
|
+
log "UNHEALTHY on $CUR${STALE_FROM:+ (kickstarted from $STALE_FROM)}: $HEALTH_REASON — no upgrade to roll back; the org cannot see this seat. maestro doctor"
|
|
573
|
+
# The `unhealthy-current:` PREFIX is load-bearing (the episode check below
|
|
574
|
+
# and failed_hold_reason both match on it), so the ahead-of-registry fact
|
|
575
|
+
# is appended, never prepended — an unpublished build that is also sick is
|
|
576
|
+
# the worst case and must say both things.
|
|
577
|
+
write_last "$CUR" "$CUR" false false "unhealthy-current: $HEALTH_REASON${AHEAD:+ (and ahead-of-registry: npm latest is $LATEST)}"
|
|
578
|
+
case "$PREV_REASON" in
|
|
579
|
+
unhealthy-current*) log "session already notified this episode (last.json was unhealthy-current); notice not rewritten" ;;
|
|
580
|
+
*) write_notice "$CUR" "$CUR" "unhealthy-current: $HEALTH_REASON" ;;
|
|
581
|
+
esac
|
|
582
|
+
fi
|
|
583
|
+
exit 0
|
|
584
|
+
fi
|
|
585
|
+
|
|
586
|
+
# ── failed-target hold ───────────────────────────────────────────────────────
|
|
587
|
+
# last.json is the memory of the health gate: a LATEST that was installed here
|
|
588
|
+
# and rolled back (or could not even be rolled back) is not retried every hour
|
|
589
|
+
# — that would be two installs, two global refreshes and two daemon restarts an
|
|
590
|
+
# hour until someone touched the kill-switch. An install-failed (network) entry
|
|
591
|
+
# does not hold, and neither does an unhealthy-current entry (its `to` is the
|
|
592
|
+
# installed version, never a LATEST). Fail-open: an unreadable last.json holds
|
|
593
|
+
# nothing.
|
|
594
|
+
FAILED_HOLD_S="${MAESTRO_AUTOUPDATE_FAILED_HOLD_S:-86400}"
|
|
595
|
+
failed_hold_reason(){ # prints "<reason> at <at>" when LATEST failed health within FAILED_HOLD_S
|
|
596
|
+
node -e '
|
|
597
|
+
const [file, latest, holdS] = process.argv.slice(1);
|
|
598
|
+
try {
|
|
599
|
+
const j = JSON.parse(require("fs").readFileSync(file, "utf8"));
|
|
600
|
+
const at = Date.parse(j.at);
|
|
601
|
+
if (j.to === latest && j.ok === false && /unhealthy/.test(String(j.reason)) && Number.isFinite(at) && Date.now() - at < Number(holdS) * 1000) process.stdout.write(`${j.reason} at ${j.at}`);
|
|
602
|
+
} catch {}
|
|
603
|
+
' "$LAST_JSON" "$LATEST" "$FAILED_HOLD_S" 2>/dev/null
|
|
604
|
+
}
|
|
605
|
+
HOLD="$(failed_hold_reason)"
|
|
606
|
+
if [ -n "$HOLD" ]; then
|
|
607
|
+
log "SKIP: $LATEST already failed the health gate here ($HOLD); holding ${FAILED_HOLD_S}s (still $CUR) — rm state/autoupdate/last.json to retry now"
|
|
608
|
+
exit 0
|
|
609
|
+
fi
|
|
610
|
+
log "UPDATE $CUR -> $LATEST"
|
|
611
|
+
|
|
214
612
|
# What -session plists THIS agent had installed BEFORE this run. A rollback
|
|
215
613
|
# reverts only a session job this run's upgrade installed — never one that
|
|
216
614
|
# was already there (that one is the operator's, and `maestro session stop`
|
|
@@ -240,35 +638,6 @@ refresh_global(){ # $1 = version — the rollback's counterpart of upgrade() ste
|
|
|
240
638
|
retry "npm install -g $PKG@$1" npm install -g "$PKG@$1" --no-audit --no-fund || log "WARN: global refresh to $1 failed — run: npm install -g $PKG@$1"
|
|
241
639
|
return 0
|
|
242
640
|
}
|
|
243
|
-
is_healthy(){ # THIS agent's daemon process alive AND a fresh boot/connect line
|
|
244
|
-
pgrep -f "$AGENT_DIR/scripts/daemon/maestro-daemon.mjs" >/dev/null 2>&1 || return 1
|
|
245
|
-
tail -80 "$DLOG" 2>/dev/null | grep -qE "org-mesh connected|daemon\] Running" || return 1
|
|
246
|
-
return 0
|
|
247
|
-
}
|
|
248
|
-
session_reconciled(){ # only asked when a -session plist was generated; never a rollback reason
|
|
249
|
-
label_loaded "$SESSION_LABEL" || { log "reconcile-failed: session job $SESSION_LABEL is not loaded (maestro session start)"; return 1; }
|
|
250
|
-
pgrep -f "$AGENT_DIR/scripts/session/supervisor" >/dev/null 2>&1 || { log "reconcile-failed: $SESSION_LABEL is loaded but no supervisor process is alive for $AGENT_DIR (maestro session status)"; return 1; }
|
|
251
|
-
return 0
|
|
252
|
-
}
|
|
253
|
-
write_upgrade_notice(){ # $1 = from, $2 = to — tell the front-door session; never restart it here
|
|
254
|
-
local dir="$AGENT_DIR/state/session" tmp
|
|
255
|
-
mkdir -p "$dir" 2>/dev/null || return 0
|
|
256
|
-
tmp="$(mktemp "$dir/.upgrade-notice.XXXXXX" 2>/dev/null)" || return 0
|
|
257
|
-
printf '{\n "from": "%s",\n "to": "%s",\n "at": "%s"\n}\n' "$1" "$2" "$(date -u +%FT%TZ)" > "$tmp" \
|
|
258
|
-
&& mv -f "$tmp" "$dir/upgrade-notice.json" \
|
|
259
|
-
&& log "session notified: upgrade-notice.json $1 -> $2 (session restarts itself when idle)"
|
|
260
|
-
rm -f "$tmp" 2>/dev/null
|
|
261
|
-
return 0
|
|
262
|
-
}
|
|
263
|
-
write_last(){ # $1 from, $2 to, $3 ok, $4 healthy, $5 reason — state/autoupdate/last.json (→ beat machine.upgrade)
|
|
264
|
-
local dir="$AGENT_DIR/state/autoupdate" tmp
|
|
265
|
-
mkdir -p "$dir" 2>/dev/null || return 0
|
|
266
|
-
tmp="$(mktemp "$dir/.last.XXXXXX" 2>/dev/null)" || return 0
|
|
267
|
-
printf '{\n "from": "%s",\n "to": "%s",\n "at": "%s",\n "ok": %s,\n "healthy": %s,\n "reason": "%s"\n}\n' \
|
|
268
|
-
"$1" "$2" "$(date -u +%FT%TZ)" "$3" "$4" "$5" > "$tmp" && mv -f "$tmp" "$dir/last.json"
|
|
269
|
-
rm -f "$tmp" 2>/dev/null
|
|
270
|
-
return 0
|
|
271
|
-
}
|
|
272
641
|
migrate_poller_plist(){ # audit F6 follow-through: an older install left SLACK_USER_TOKEN in a 0644 poller plist; nothing else rewrites it
|
|
273
642
|
[ -f "$AGENT_DIR/scripts/poller-launchd/migrate.sh" ] || return 0
|
|
274
643
|
bash "$AGENT_DIR/scripts/poller-launchd/migrate.sh" >> "$LOG" 2>&1 || log "WARN: poller plist migration nonzero (see above)"
|
|
@@ -287,11 +656,12 @@ if is_healthy; then
|
|
|
287
656
|
REASON=""
|
|
288
657
|
if [ -n "$SESSION_LABEL" ] && ! session_reconciled; then REASON="session-reconcile-failed"; fi
|
|
289
658
|
log "OK: healthy on $LATEST${REASON:+ ($REASON)}"
|
|
659
|
+
sibling_job_audit
|
|
290
660
|
write_upgrade_notice "$CUR" "$LATEST"
|
|
291
661
|
write_last "$CUR" "$LATEST" true true "$REASON"
|
|
292
662
|
migrate_poller_plist
|
|
293
663
|
else
|
|
294
|
-
log "UNHEALTHY on $LATEST -> ROLLBACK to $CUR"
|
|
664
|
+
log "UNHEALTHY on $LATEST ($HEALTH_REASON) -> ROLLBACK to $CUR"
|
|
295
665
|
apply_version "$CUR" || log "WARN: rollback install nonzero"
|
|
296
666
|
refresh_global "$CUR"
|
|
297
667
|
revert_session_install
|
|
@@ -301,7 +671,7 @@ else
|
|
|
301
671
|
log "rolled back to $CUR (healthy)"
|
|
302
672
|
write_last "$CUR" "$LATEST" false true "unhealthy-rolled-back"
|
|
303
673
|
else
|
|
304
|
-
log "ROLLBACK health still failing on $CUR — needs operator"
|
|
674
|
+
log "ROLLBACK health still failing on $CUR ($HEALTH_REASON) — needs operator"
|
|
305
675
|
write_last "$CUR" "$LATEST" false false "rollback-unhealthy"
|
|
306
676
|
fi
|
|
307
677
|
fi
|
|
@@ -35,6 +35,19 @@ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
|
35
35
|
DATE=$(date +%Y-%m-%d)
|
|
36
36
|
LOG_FILE="$LOG_DIR/$DATE-$TRIGGER_NAME.jsonl"
|
|
37
37
|
|
|
38
|
+
# Retired on engine-cohort seats (W4-E1): this lane runs claude outside the
|
|
39
|
+
# runtime adapter. The guard exits 3 with the reason when runtime.engine is cohort.
|
|
40
|
+
# Fail closed: when the guard cannot run (no node on PATH — launchd's
|
|
41
|
+
# /usr/bin:/bin — or the guard file is missing), config/agent.json is checked
|
|
42
|
+
# here and an engine-cohort seat is refused all the same.
|
|
43
|
+
LEGACY_GUARD="$AGENT_REPO_DIR/lib/runtime/legacy-shell-guard.mjs"
|
|
44
|
+
if [ -f "$LEGACY_GUARD" ] && command -v node >/dev/null 2>&1; then
|
|
45
|
+
node "$LEGACY_GUARD" run-trigger.sh "$AGENT_REPO_DIR" || exit $?
|
|
46
|
+
elif [ -f "$AGENT_REPO_DIR/config/agent.json" ] && grep -Eq '"engine"[[:space:]]*:[[:space:]]*"cohort"' "$AGENT_REPO_DIR/config/agent.json"; then
|
|
47
|
+
echo "run-trigger.sh is retired on this seat: runtime.engine is \"cohort\", and this legacy lane runs the claude binary directly, outside the runtime adapter (the engine guard could not run: node or $LEGACY_GUARD is unavailable)." >&2
|
|
48
|
+
exit 3
|
|
49
|
+
fi
|
|
50
|
+
|
|
38
51
|
# Check emergency stop
|
|
39
52
|
if [ -f "$AGENT_REPO_DIR/.emergency-stop" ]; then
|
|
40
53
|
echo "{\"timestamp\":\"$TIMESTAMP\",\"trigger\":\"$TRIGGER_NAME\",\"status\":\"blocked\",\"reason\":\"emergency-stop\"}" >> "$LOG_FILE"
|