@cohortapp/agent-sdk 2.17.0 → 2.18.5
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-slots.mjs +234 -0
- package/lib/collective/capture.mjs +8 -6
- package/lib/collective/config.mjs +2 -0
- package/lib/collective/global-config.mjs +63 -1
- package/lib/collective/loop-guard.mjs +155 -0
- 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 +833 -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 +229 -11
- 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/scaffold/config/collective.yaml +7 -0
- package/scripts/ci/check-durable-write-seam.mjs +3 -1
- 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 +142 -19
- 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 +1151 -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 +465 -81
- 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
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/collective/capture-slots.mjs — bound the capture workers a machine runs.
|
|
3
|
+
*
|
|
4
|
+
* Every Stop/PreCompact on the machine spawns a detached capture worker, and
|
|
5
|
+
* each worker runs one or two `claude --print` passes (hundreds of MB of RSS
|
|
6
|
+
* apiece). With no ceiling, a burst of session ends — or any loop that feeds
|
|
7
|
+
* the hook — becomes an unbounded fan-out of model processes. Two locks keep it
|
|
8
|
+
* bounded:
|
|
9
|
+
*
|
|
10
|
+
* - SLOT locks — `slot-<i>.lock` for i < max. A worker must hold one to run
|
|
11
|
+
* its passes, so at most `max` capture workers do model work machine-wide.
|
|
12
|
+
* - SESSION locks — `session-<id>.lock`. One session is captured by at most
|
|
13
|
+
* one worker at a time (a PreCompact and the Stop that follows it must not
|
|
14
|
+
* distil the same transcript twice in parallel).
|
|
15
|
+
*
|
|
16
|
+
* Both are O_EXCL files (`{flag:"wx"}` — the lock idiom registered in
|
|
17
|
+
* scripts/ci/check-durable-write-seam.mjs) under ONE machine-wide directory, so
|
|
18
|
+
* two agent installs on a machine share the ceiling. A lock records its holder's
|
|
19
|
+
* pid; a lock whose pid is dead (or which is older than `maxAgeMs`, covering
|
|
20
|
+
* pid reuse) is reclaimed. Reclaim re-reads the lock and deletes it only if it
|
|
21
|
+
* still holds the exact bytes judged stale, so a lock freshly re-taken by
|
|
22
|
+
* another reclaimer is not deleted; the residual window between that read and
|
|
23
|
+
* the unlink can at worst admit one extra worker, never lose the ceiling.
|
|
24
|
+
*
|
|
25
|
+
* Expected failure is a returned `{ok:false, error}`; nothing here throws.
|
|
26
|
+
*
|
|
27
|
+
* @module lib/collective/capture-slots
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
"use strict";
|
|
31
|
+
|
|
32
|
+
import { mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
33
|
+
import { homedir } from "node:os";
|
|
34
|
+
import { dirname, join } from "node:path";
|
|
35
|
+
import { randomBytes } from "node:crypto";
|
|
36
|
+
|
|
37
|
+
/** Default machine-wide ceiling on capture workers doing model work. */
|
|
38
|
+
export const DEFAULT_MAX_CONCURRENT = 2;
|
|
39
|
+
/** Default time a worker waits for a slot before recording a skipped capture. */
|
|
40
|
+
export const DEFAULT_QUEUE_WAIT_MS = 30_000;
|
|
41
|
+
/** Poll interval while queued for a slot. */
|
|
42
|
+
export const DEFAULT_POLL_MS = 1_000;
|
|
43
|
+
/**
|
|
44
|
+
* A lock older than this is stale even if its pid is alive (the pid was
|
|
45
|
+
* reused). Well above a worker's worst case: distil (60s) + reflect (90s) +
|
|
46
|
+
* index + queue wait.
|
|
47
|
+
*/
|
|
48
|
+
export const DEFAULT_LOCK_MAX_AGE_MS = 15 * 60_000;
|
|
49
|
+
/** A lock with unreadable content younger than this is a holder mid-write, not a corpse. */
|
|
50
|
+
const UNREADABLE_GRACE_MS = 10_000;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The machine-wide lock directory. `MAESTRO_CAPTURE_LOCK_DIR` overrides it
|
|
54
|
+
* (tests, and a machine that wants the locks elsewhere).
|
|
55
|
+
* @param {Record<string, string|undefined>} [env]
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function captureLockDir(env = process.env) {
|
|
59
|
+
const override = env && env.MAESTRO_CAPTURE_LOCK_DIR;
|
|
60
|
+
return override && String(override).trim() ? String(override) : join(homedir(), ".claude", "maestro-capture-locks");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The configured ceiling, clamped to a positive integer. Absent/invalid → default.
|
|
65
|
+
* @param {object} [cfg] collective config
|
|
66
|
+
* @returns {number}
|
|
67
|
+
*/
|
|
68
|
+
export function maxConcurrentFrom(cfg) {
|
|
69
|
+
const n = Number(cfg && cfg.captureMaxConcurrent);
|
|
70
|
+
return Number.isFinite(n) && n >= 1 ? Math.floor(n) : DEFAULT_MAX_CONCURRENT;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The configured queue wait in ms, clamped to >= 0. Absent/invalid → default.
|
|
75
|
+
* @param {object} [cfg] collective config
|
|
76
|
+
* @returns {number}
|
|
77
|
+
*/
|
|
78
|
+
export function queueWaitFrom(cfg) {
|
|
79
|
+
const n = Number(cfg && cfg.captureQueueWaitMs);
|
|
80
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_QUEUE_WAIT_MS;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Is `pid` a live process? EPERM means alive (someone else's process). */
|
|
84
|
+
export function pidAlive(pid) {
|
|
85
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
86
|
+
try {
|
|
87
|
+
process.kill(pid, 0);
|
|
88
|
+
return true;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
return !!err && err.code === "EPERM";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function sanitize(id) {
|
|
95
|
+
const s = String(id || "").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 128);
|
|
96
|
+
return s || "unknown";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Path of the session lock for `sessionId`. */
|
|
100
|
+
export function sessionLockPath(dir, sessionId) {
|
|
101
|
+
return join(dir, `session-${sanitize(sessionId)}.lock`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Path of slot `i`. */
|
|
105
|
+
export function slotLockPath(dir, i) {
|
|
106
|
+
return join(dir, `slot-${i}.lock`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Is the lock whose raw bytes are `raw` (mtime `mtimeMs`) stale at `nowMs`?
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
function isStale(raw, mtimeMs, nowMs, deps) {
|
|
114
|
+
let holder = null;
|
|
115
|
+
try { holder = JSON.parse(raw); } catch { holder = null; }
|
|
116
|
+
if (!holder || !Number.isInteger(holder.pid)) {
|
|
117
|
+
// Empty/corrupt: a holder between open(wx) and write, or a crashed one.
|
|
118
|
+
return nowMs - mtimeMs > UNREADABLE_GRACE_MS;
|
|
119
|
+
}
|
|
120
|
+
const acquired = Date.parse(holder.acquiredAt || "");
|
|
121
|
+
const ageMs = Number.isFinite(acquired) ? nowMs - acquired : nowMs - mtimeMs;
|
|
122
|
+
if (ageMs > deps.maxAgeMs) return true;
|
|
123
|
+
return !deps.isAlive(holder.pid);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Try once to take the O_EXCL lock at `lockPath`, reclaiming a stale one.
|
|
128
|
+
*
|
|
129
|
+
* @param {string} lockPath
|
|
130
|
+
* @param {{pid?:number, sessionId?:string}} [owner]
|
|
131
|
+
* @param {{now?:()=>number, isAlive?:(pid:number)=>boolean, maxAgeMs?:number}} [deps]
|
|
132
|
+
* @returns {{ok:true, lockPath:string, release:()=>boolean, reclaimed:boolean} |
|
|
133
|
+
* {ok:false, error:{code:"held"|"io", message:string, holder?:object}}}
|
|
134
|
+
*/
|
|
135
|
+
export function tryAcquireLock(lockPath, owner = {}, deps = {}) {
|
|
136
|
+
const d = {
|
|
137
|
+
now: typeof deps.now === "function" ? deps.now : Date.now,
|
|
138
|
+
isAlive: typeof deps.isAlive === "function" ? deps.isAlive : pidAlive,
|
|
139
|
+
maxAgeMs: Number.isFinite(deps.maxAgeMs) ? deps.maxAgeMs : DEFAULT_LOCK_MAX_AGE_MS,
|
|
140
|
+
};
|
|
141
|
+
const pid = Number.isInteger(owner.pid) ? owner.pid : process.pid;
|
|
142
|
+
const token = randomBytes(8).toString("hex");
|
|
143
|
+
const dir = dirname(lockPath);
|
|
144
|
+
try { mkdirSync(dir, { recursive: true }); } catch (err) {
|
|
145
|
+
return { ok: false, error: { code: "io", message: `lock dir: ${err && err.message}` } };
|
|
146
|
+
}
|
|
147
|
+
let reclaimed = false;
|
|
148
|
+
// Two attempts: the first may find a stale lock and reclaim it; the second
|
|
149
|
+
// takes the freed name (or loses it to a concurrent reclaimer, which is "held").
|
|
150
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
151
|
+
try {
|
|
152
|
+
writeFileSync(lockPath, JSON.stringify({ pid, sessionId: owner.sessionId || "", token, acquiredAt: new Date(d.now()).toISOString() }), { flag: "wx" });
|
|
153
|
+
const release = () => {
|
|
154
|
+
try {
|
|
155
|
+
const cur = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
156
|
+
if (!cur || cur.token !== token) return false; // reclaimed from us after we went stale — not ours to delete
|
|
157
|
+
unlinkSync(lockPath);
|
|
158
|
+
return true;
|
|
159
|
+
} catch {
|
|
160
|
+
return false; // already gone (reclaimed or cleaned) — nothing to release
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
return { ok: true, lockPath, release, reclaimed };
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (!err || err.code !== "EEXIST") {
|
|
166
|
+
return { ok: false, error: { code: "io", message: `lock ${lockPath}: ${err && err.message}` } };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
let raw = "";
|
|
170
|
+
let mtimeMs = 0;
|
|
171
|
+
try {
|
|
172
|
+
raw = readFileSync(lockPath, "utf8");
|
|
173
|
+
mtimeMs = statSync(lockPath).mtimeMs;
|
|
174
|
+
} catch {
|
|
175
|
+
continue; // the holder released between our create and read — retry the create
|
|
176
|
+
}
|
|
177
|
+
if (!isStale(raw, mtimeMs, d.now(), d)) {
|
|
178
|
+
let holder = null;
|
|
179
|
+
try { holder = JSON.parse(raw); } catch { holder = null; }
|
|
180
|
+
return { ok: false, error: { code: "held", message: `lock ${lockPath} is held`, holder: holder || undefined } };
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
// Delete only the exact stale bytes we judged; a lock re-taken since is left alone.
|
|
184
|
+
if (readFileSync(lockPath, "utf8") === raw) { unlinkSync(lockPath); reclaimed = true; }
|
|
185
|
+
} catch { /* someone else reclaimed it first — the retry below races for the name fairly */ }
|
|
186
|
+
}
|
|
187
|
+
return { ok: false, error: { code: "held", message: `lock ${lockPath} is held (lost the reclaim race)` } };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Take any free slot among `max`, once.
|
|
192
|
+
* @param {string} dir
|
|
193
|
+
* @param {number} max
|
|
194
|
+
* @param {object} [owner]
|
|
195
|
+
* @param {object} [deps] as {@link tryAcquireLock}
|
|
196
|
+
* @returns {{ok:true, slot:number, release:()=>boolean, reclaimed:boolean} |
|
|
197
|
+
* {ok:false, error:{code:"no-slot"|"io", message:string}}}
|
|
198
|
+
*/
|
|
199
|
+
export function tryAcquireSlot(dir, max, owner = {}, deps = {}) {
|
|
200
|
+
let ioError = null;
|
|
201
|
+
for (let i = 0; i < max; i++) {
|
|
202
|
+
const r = tryAcquireLock(slotLockPath(dir, i), owner, deps);
|
|
203
|
+
if (r.ok) return { ok: true, slot: i, release: r.release, reclaimed: r.reclaimed };
|
|
204
|
+
if (r.error.code === "io") ioError = r.error;
|
|
205
|
+
}
|
|
206
|
+
if (ioError) return { ok: false, error: ioError };
|
|
207
|
+
return { ok: false, error: { code: "no-slot", message: `all ${max} capture slots are held` } };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Wait up to `waitMs` for a slot, polling. Resolves `{ok:false, error:{code:"no-slot"}}`
|
|
212
|
+
* when the wait runs out — the caller records the skipped capture.
|
|
213
|
+
* @param {string} dir
|
|
214
|
+
* @param {number} max
|
|
215
|
+
* @param {object} [owner]
|
|
216
|
+
* @param {{waitMs?:number, pollMs?:number, sleep?:(ms:number)=>Promise<void>, now?:()=>number}} [opts]
|
|
217
|
+
* plus the {@link tryAcquireLock} deps
|
|
218
|
+
* @returns {Promise<{ok:true, slot:number, release:()=>boolean, waitedMs:number} |
|
|
219
|
+
* {ok:false, error:{code:string, message:string}, waitedMs:number}>}
|
|
220
|
+
*/
|
|
221
|
+
export async function acquireSlotQueued(dir, max, owner = {}, opts = {}) {
|
|
222
|
+
const now = typeof opts.now === "function" ? opts.now : Date.now;
|
|
223
|
+
const sleep = typeof opts.sleep === "function" ? opts.sleep : (ms) => new Promise((r) => setTimeout(r, ms));
|
|
224
|
+
const waitMs = Number.isFinite(opts.waitMs) ? Math.max(0, opts.waitMs) : DEFAULT_QUEUE_WAIT_MS;
|
|
225
|
+
const pollMs = Number.isFinite(opts.pollMs) && opts.pollMs > 0 ? opts.pollMs : DEFAULT_POLL_MS;
|
|
226
|
+
const start = now();
|
|
227
|
+
for (;;) {
|
|
228
|
+
const r = tryAcquireSlot(dir, max, owner, opts);
|
|
229
|
+
if (r.ok) return { ...r, waitedMs: now() - start };
|
|
230
|
+
if (r.error.code !== "no-slot") return { ...r, waitedMs: now() - start };
|
|
231
|
+
if (now() - start >= waitMs) return { ...r, waitedMs: now() - start };
|
|
232
|
+
await sleep(pollMs);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { spawnSync } from "node:child_process";
|
|
23
23
|
import { normalizeCard, appendCards, repoLabel } from "./cards.mjs";
|
|
24
24
|
import { isRepoDenied } from "./config.mjs";
|
|
25
|
-
import {
|
|
25
|
+
import { buildSpawn } from "../runtime/adapter.mjs";
|
|
26
26
|
|
|
27
27
|
const DISTILL_TIMEOUT_MS = 60_000;
|
|
28
28
|
const MAX_TRANSCRIPT_CHARS = 16_000;
|
|
@@ -118,13 +118,15 @@ export function extractCardsFromModelOutput(text) {
|
|
|
118
118
|
/** Default distiller: spawn a cheap `claude --print` pass. Fail-open → "". */
|
|
119
119
|
export function defaultDistiller(prompt, opts = {}) {
|
|
120
120
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
121
|
+
// argv + env from the runtime adapter (lane "capture"): --print --model <m>
|
|
122
|
+
// <daemon MCP args>, prompt on stdin, PATH augmented.
|
|
123
|
+
const spec = buildSpawn({ lane: "capture", model: opts.model || "haiku", prompt, mcp: {}, env: process.env });
|
|
124
|
+
if (!spec.ok) return "";
|
|
125
|
+
const res = spawnSync(spec.bin, spec.argv, {
|
|
126
|
+
input: spec.stdin,
|
|
125
127
|
encoding: "utf8",
|
|
126
128
|
timeout: DISTILL_TIMEOUT_MS,
|
|
127
|
-
env:
|
|
129
|
+
env: spec.env,
|
|
128
130
|
maxBuffer: 8 * 1024 * 1024,
|
|
129
131
|
});
|
|
130
132
|
if (res.status === 0 && res.stdout) return res.stdout;
|
|
@@ -27,6 +27,8 @@ export const DEFAULTS = Object.freeze({
|
|
|
27
27
|
captureOn: ["Stop", "PreCompact"],
|
|
28
28
|
minTurnsToCapture: 2,
|
|
29
29
|
maxCardsPerSession: 12,
|
|
30
|
+
captureMaxConcurrent: 2,
|
|
31
|
+
captureQueueWaitMs: 30000,
|
|
30
32
|
repoDenylist: [],
|
|
31
33
|
digestBudgetTokens: 1500,
|
|
32
34
|
recencyHalfLifeDays: 30,
|
|
@@ -200,14 +200,25 @@ export function buildIdentityClaudeMd(agentJson, o = {}) {
|
|
|
200
200
|
* Default command is the bare `cohort-mcp` (the global npm install); a caller
|
|
201
201
|
* that knows it is not on PATH passes `{command, args}` (e.g. the current node
|
|
202
202
|
* + this package's bin/cohort-mcp.mjs) so the server still starts. Pure.
|
|
203
|
+
*
|
|
204
|
+
* `inheritEnv` (CF-22, W5-B): the Cohort Engine gives no MCP server a
|
|
205
|
+
* credential its entry does not name, so the entry names the org credential
|
|
206
|
+
* cohort-mcp resolves from the environment (lib/org/client.mjs, both brands —
|
|
207
|
+
* bin/cohort-mcp.mjs bridges NEOLITH_* at boot). An enrolled seat's cohort-mcp
|
|
208
|
+
* reads its token from config/org.yaml first; the list keeps an env-only seat
|
|
209
|
+
* working. The claude binary tolerates the key; `claude mcp add-json` drops it,
|
|
210
|
+
* so global-setup's verify step falls back to the JSON merge, which keeps it.
|
|
203
211
|
* @param {string} agentRoot @param {{command?:string, args?:string[]}} [o]
|
|
204
212
|
*/
|
|
205
213
|
export function cohortMcpEntry(agentRoot, o = {}) {
|
|
206
214
|
const command = o.command ? String(o.command) : "cohort-mcp";
|
|
207
215
|
const args = Array.isArray(o.args) ? o.args.map(String) : [];
|
|
208
|
-
return { command, args, env: { COHORT_AGENT_ROOT: String(agentRoot || "") } };
|
|
216
|
+
return { command, args, env: { COHORT_AGENT_ROOT: String(agentRoot || "") }, inheritEnv: [...COHORT_MCP_INHERIT_ENV] };
|
|
209
217
|
}
|
|
210
218
|
|
|
219
|
+
/** The org credential names cohort-mcp's entry inherits (= lib/engine/context/child-env.mjs ORG_CREDENTIAL_ENV). */
|
|
220
|
+
export const COHORT_MCP_INHERIT_ENV = Object.freeze(["COHORT_API_TOKEN", "COHORT_TOKEN", "COHORT_API_KEY", "NEOLITH_API_TOKEN", "NEOLITH_TOKEN", "NEOLITH_API_KEY"]);
|
|
221
|
+
|
|
211
222
|
function clone(v) { return JSON.parse(JSON.stringify(v)); }
|
|
212
223
|
function isObj(v) { return v && typeof v === "object" && !Array.isArray(v); }
|
|
213
224
|
|
|
@@ -305,6 +316,57 @@ export function mergeSessionSettings(settings, o = {}) {
|
|
|
305
316
|
return { settings: out, changes };
|
|
306
317
|
}
|
|
307
318
|
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// State-YAML gates (2026-09): the two hooks that keep an agent's state/queues,
|
|
321
|
+
// state/dashboards and state/indexes YAML parseable and un-clobbered. They live
|
|
322
|
+
// in the SEAT's .claude/settings.json (commands are seat-relative and only make
|
|
323
|
+
// sense inside an agent repo), never in ~/.claude. A hand edit to that file does
|
|
324
|
+
// not survive a fresh checkout (the scaffold copy is what `create` ships) or an
|
|
325
|
+
// upgrade (settings.json is not on the upgrade path), so the scaffold carries
|
|
326
|
+
// them for new seats and `global-setup` — which `upgrade` runs as a post-step —
|
|
327
|
+
// merges them onto existing ones with this helper. Pure; I/O is in bin/maestro.
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
/** The two hook groups, in the exact shape the scaffold .claude/settings.json carries. */
|
|
331
|
+
export const STATE_YAML_GATE_HOOKS = Object.freeze([
|
|
332
|
+
Object.freeze({
|
|
333
|
+
event: "PreToolUse",
|
|
334
|
+
matcher: "Write|Edit",
|
|
335
|
+
marker: "scripts/hooks/pre-write-yaml-validate.mjs",
|
|
336
|
+
command: "node ./scripts/hooks/pre-write-yaml-validate.mjs",
|
|
337
|
+
}),
|
|
338
|
+
Object.freeze({
|
|
339
|
+
event: "PostToolUse",
|
|
340
|
+
matcher: "Bash|Write|Edit",
|
|
341
|
+
marker: "scripts/hooks/validate-state-yaml.sh",
|
|
342
|
+
command: "./scripts/hooks/validate-state-yaml.sh",
|
|
343
|
+
}),
|
|
344
|
+
]);
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Merge the state-YAML gate hook groups into a seat's settings.json object, once.
|
|
348
|
+
* Presence is judged by the script marker anywhere under the event (any matcher),
|
|
349
|
+
* so an operator who already wired the script by hand is left alone. Existing
|
|
350
|
+
* groups, permissions and every other key are preserved. Idempotent. Pure.
|
|
351
|
+
*
|
|
352
|
+
* @param {object} settings parsed .claude/settings.json ({} / null when absent)
|
|
353
|
+
* @returns {{ settings: object, added: string[] }} added = "PreToolUse:pre-write-yaml-validate" ...
|
|
354
|
+
*/
|
|
355
|
+
export function mergeStateYamlGateHooks(settings) {
|
|
356
|
+
const out = isObj(settings) ? clone(settings) : {};
|
|
357
|
+
const added = [];
|
|
358
|
+
if (!isObj(out.hooks)) out.hooks = {};
|
|
359
|
+
for (const g of STATE_YAML_GATE_HOOKS) {
|
|
360
|
+
if (!Array.isArray(out.hooks[g.event])) out.hooks[g.event] = [];
|
|
361
|
+
const present = out.hooks[g.event].some((grp) =>
|
|
362
|
+
grp && Array.isArray(grp.hooks) && grp.hooks.some((h) => h && typeof h.command === "string" && h.command.includes(g.marker)));
|
|
363
|
+
if (present) continue;
|
|
364
|
+
out.hooks[g.event].push({ matcher: g.matcher, hooks: [{ type: "command", command: g.command }] });
|
|
365
|
+
added.push(`${g.event}:${basename(g.marker).replace(/\.(mjs|sh)$/, "")}`);
|
|
366
|
+
}
|
|
367
|
+
return { settings: out, added };
|
|
368
|
+
}
|
|
369
|
+
|
|
308
370
|
/** Build the machine pointer object. */
|
|
309
371
|
export function buildPointer(repoRoot, agentName, nowMs) {
|
|
310
372
|
return {
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/collective/loop-guard.mjs — keep the collective layer from capturing itself.
|
|
3
|
+
*
|
|
4
|
+
* The global hooks (`maestro global-setup`) fire on EVERY Claude Code session on
|
|
5
|
+
* the machine, and the capture worker's distiller, the reflect reviewer and the
|
|
6
|
+
* skill curator are themselves `claude --print` sessions. Without an exemption
|
|
7
|
+
* each of those children's Stop hook captures ITS OWN transcript — whose content
|
|
8
|
+
* is the capture prompt — which spawns another worker, whose children stop and
|
|
9
|
+
* capture again. That is a fork-bomb: nothing in the chain terminates it.
|
|
10
|
+
*
|
|
11
|
+
* Two independent signals mark a session as the collective layer's own child:
|
|
12
|
+
*
|
|
13
|
+
* 1. ENV — every collective spawn carries {@link COLLECTIVE_CHILD_ENV}=1
|
|
14
|
+
* (set by the runtime adapter for the capture/reflect/curator lanes, and by
|
|
15
|
+
* the capture worker for its whole process tree). The hook classifies such
|
|
16
|
+
* a session as kind `collective` and does nothing for it.
|
|
17
|
+
* 2. CONTENT — if the env did not propagate (an older runtime, a wrapper that
|
|
18
|
+
* scrubs the environment), the transcript's FIRST user message is one of
|
|
19
|
+
* the collective prompts. Those prompts are this module's own text, so the
|
|
20
|
+
* opener is a reliable fingerprint; a human session does not open with it.
|
|
21
|
+
*
|
|
22
|
+
* Pure except {@link readTranscriptHead}, which reads a bounded prefix of a file.
|
|
23
|
+
*
|
|
24
|
+
* @module lib/collective/loop-guard
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
"use strict";
|
|
28
|
+
|
|
29
|
+
import { openSync, readSync, closeSync } from "node:fs";
|
|
30
|
+
|
|
31
|
+
/** The env marker every `claude` process spawned by the collective layer carries. */
|
|
32
|
+
export const COLLECTIVE_CHILD_ENV = "MAESTRO_COLLECTIVE_CHILD";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The opening line of each collective prompt. A drift test pins each one to its
|
|
36
|
+
* prompt builder (capture.buildDistillPrompt, reflect.buildReviewPrompt,
|
|
37
|
+
* curator.buildConsolidationPrompt), so a reworded prompt cannot silently disarm
|
|
38
|
+
* the content fallback.
|
|
39
|
+
*/
|
|
40
|
+
export const COLLECTIVE_PROMPT_SENTINELS = Object.freeze([
|
|
41
|
+
"You distil a finished work session into durable MEMORY CARDS",
|
|
42
|
+
"You are the self-improvement reviewer for an autonomous AI agent",
|
|
43
|
+
"You are the skill CURATOR for an autonomous AI agent",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* How much of a transcript the content fallback reads. The prompt is the first
|
|
48
|
+
* user input; SessionStart attachments (skill listings, digests) can precede it
|
|
49
|
+
* by tens of KB, so the window is generous — and one bounded read per stop.
|
|
50
|
+
*/
|
|
51
|
+
export const TRANSCRIPT_HEAD_BYTES = 512 * 1024;
|
|
52
|
+
|
|
53
|
+
/** How far into the first user message a sentinel may start (tolerates a harness preamble). */
|
|
54
|
+
const SENTINEL_WINDOW_CHARS = 2000;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Does this environment mark a collective child?
|
|
58
|
+
* @param {Record<string, string|undefined>} [env]
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
export function isCollectiveChildEnv(env = process.env) {
|
|
62
|
+
return !!env && String(env[COLLECTIVE_CHILD_ENV] || "").trim() === "1";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The session kind the hooks act on. `collective` wins over `daemon`: a
|
|
67
|
+
* collective child spawned from a daemon session inherits MAESTRO_DAEMON=1 and
|
|
68
|
+
* must still be exempt from capture.
|
|
69
|
+
* @param {{source?:string}} [input] the hook's stdin payload
|
|
70
|
+
* @param {Record<string, string|undefined>} [env]
|
|
71
|
+
* @returns {"collective"|"daemon"|"interactive"}
|
|
72
|
+
*/
|
|
73
|
+
export function classifyKind(input = {}, env = process.env) {
|
|
74
|
+
if (isCollectiveChildEnv(env)) return "collective";
|
|
75
|
+
const src = String((input && input.source) || "").toLowerCase();
|
|
76
|
+
if (src.includes("daemon") || (env && env.MAESTRO_DAEMON === "1")) return "daemon";
|
|
77
|
+
return "interactive";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function messageText(obj) {
|
|
81
|
+
const c = obj.content ?? (obj.message && obj.message.content);
|
|
82
|
+
if (typeof c === "string") return c;
|
|
83
|
+
if (Array.isArray(c)) {
|
|
84
|
+
return c.map((p) => (typeof p === "string" ? p : p && typeof p.text === "string" ? p.text : "")).filter(Boolean).join(" ");
|
|
85
|
+
}
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The text of the first user message in a JSONL transcript prefix, or "".
|
|
91
|
+
* A truncated last line is simply skipped.
|
|
92
|
+
* @param {string} head
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
export function firstUserMessage(head) {
|
|
96
|
+
if (!head) return "";
|
|
97
|
+
for (const raw of String(head).split("\n")) {
|
|
98
|
+
const line = raw.trim();
|
|
99
|
+
if (!line.startsWith("{")) continue;
|
|
100
|
+
let obj;
|
|
101
|
+
try { obj = JSON.parse(line); } catch { continue; /* a truncated/partial line is not a message */ }
|
|
102
|
+
// A `--print` prompt is recorded twice: first as the queued input
|
|
103
|
+
// (`queue-operation`/`enqueue`, ahead of any attachment lines), then as the
|
|
104
|
+
// first `user` message. Either one is the session's first user input.
|
|
105
|
+
const queued = obj.type === "queue-operation" && obj.operation === "enqueue";
|
|
106
|
+
const role = (obj.message && obj.message.role) || obj.role || obj.type || "";
|
|
107
|
+
if (role !== "user" && !queued) continue;
|
|
108
|
+
const text = messageText(obj);
|
|
109
|
+
if (text) return text;
|
|
110
|
+
}
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Is this transcript a collective child's (its first user turn is a collective prompt)?
|
|
116
|
+
* @param {string} head a prefix of the transcript JSONL
|
|
117
|
+
* @returns {boolean}
|
|
118
|
+
*/
|
|
119
|
+
export function isCollectivePromptTranscript(head) {
|
|
120
|
+
const first = firstUserMessage(head).slice(0, SENTINEL_WINDOW_CHARS);
|
|
121
|
+
if (!first) return false;
|
|
122
|
+
return COLLECTIVE_PROMPT_SENTINELS.some((s) => first.includes(s));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Read at most `bytes` from the start of a file. "" when absent/unreadable.
|
|
127
|
+
* @param {string} path
|
|
128
|
+
* @param {number} [bytes]
|
|
129
|
+
* @returns {string}
|
|
130
|
+
*/
|
|
131
|
+
export function readTranscriptHead(path, bytes = TRANSCRIPT_HEAD_BYTES) {
|
|
132
|
+
if (!path) return "";
|
|
133
|
+
let fd = null;
|
|
134
|
+
try {
|
|
135
|
+
fd = openSync(path, "r");
|
|
136
|
+
const buf = Buffer.alloc(bytes);
|
|
137
|
+
const n = readSync(fd, buf, 0, bytes, 0);
|
|
138
|
+
return buf.subarray(0, n).toString("utf8");
|
|
139
|
+
} catch {
|
|
140
|
+
return ""; // no transcript → the content fallback has nothing to say; the env signal still governs
|
|
141
|
+
} finally {
|
|
142
|
+
if (fd !== null) { try { closeSync(fd); } catch { /* the read already completed or failed */ } }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Why a stop/capture event must NOT spawn a capture worker, or null when it may.
|
|
148
|
+
* @param {{kind:string, transcriptHead?:string}} o
|
|
149
|
+
* @returns {null|"collective-child-env"|"collective-child-transcript"}
|
|
150
|
+
*/
|
|
151
|
+
export function captureExemption({ kind, transcriptHead = "" } = {}) {
|
|
152
|
+
if (kind === "collective") return "collective-child-env";
|
|
153
|
+
if (isCollectivePromptTranscript(transcriptHead)) return "collective-child-transcript";
|
|
154
|
+
return null;
|
|
155
|
+
}
|