@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
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/tools/bash-background.mjs — the session's shell: a persistent
|
|
3
|
+
* working directory for Bash, background commands, BashOutput and KillShell.
|
|
4
|
+
*
|
|
5
|
+
* Persistent working directory. Each foreground Bash call runs in the
|
|
6
|
+
* directory the previous call ended in, so `cd build` in one call holds for
|
|
7
|
+
* the next. The command is followed by a capture line that prints a per-session
|
|
8
|
+
* marker and `pwd -P`; the marker line is removed from the output and the path
|
|
9
|
+
* becomes the next call's directory. A command that exits the shell early, is
|
|
10
|
+
* killed, or overflows the output limit leaves the directory unchanged. The
|
|
11
|
+
* permission check and hooks see the command as the model wrote it. A
|
|
12
|
+
* directory that no longer exists falls back to the project directory. Every
|
|
13
|
+
* subagent starts in the project directory with its own tracked directory.
|
|
14
|
+
*
|
|
15
|
+
* Background commands. `Bash {run_in_background:true}` starts `bash -c` in its
|
|
16
|
+
* own process group (same scrubbed environment as a foreground call:
|
|
17
|
+
* `scrubToolEnv`), with stdin closed, and returns a shell id at once.
|
|
18
|
+
* BashOutput {bash_id, filter?} the output produced since the last read,
|
|
19
|
+
* optionally only the lines matching `filter`
|
|
20
|
+
* (a regular expression; other lines are consumed)
|
|
21
|
+
* KillShell {shell_id} SIGTERM to the whole group, SIGKILL after 2 s
|
|
22
|
+
* Unread output is kept in memory up to 30,000 characters; beyond that it, and
|
|
23
|
+
* everything after it, is written to `<spill dir>/<shell id>.log`. Reads then
|
|
24
|
+
* return at most 30,000 bytes at a time (cut at a line end) and name the file.
|
|
25
|
+
* The spill file stops growing at 50 MB (`SPILL_FILE_CAP`); later output is
|
|
26
|
+
* dropped and reads say so. Every background process group is killed when the
|
|
27
|
+
* engine exits (`killAll`, and a process `exit` listener as the backstop).
|
|
28
|
+
*
|
|
29
|
+
* A kill never waits on a process that left the group. After SIGKILL and a
|
|
30
|
+
* short settle, the shell's pipes are destroyed and the kill returns, even if a
|
|
31
|
+
* `setsid` grandchild still holds them open.
|
|
32
|
+
*
|
|
33
|
+
* CF-48 (W5-B): that grandchild no longer survives. Every shell (background
|
|
34
|
+
* Bash, Monitor, a subagent's or workflow child's shell — they share this
|
|
35
|
+
* manager) is tracked by process tree, not only by group (process-tree.mjs):
|
|
36
|
+
* its environment carries a random COHORT_ENGINE_PROCESS_TAG, its descendants
|
|
37
|
+
* are recorded with their start identity shortly after it starts, while it
|
|
38
|
+
* runs and when it is stopped, and KillShell, a monitor's timeout, killAll and
|
|
39
|
+
* the exit backstop SIGTERM → SIGKILL every recorded process whose identity
|
|
40
|
+
* still matches — within the same grace period. killAll also reaps what shells
|
|
41
|
+
* that already finished left behind.
|
|
42
|
+
*
|
|
43
|
+
* `wrapWithCwdCapture`, `splitCwdCapture` and `filterLines` are pure.
|
|
44
|
+
*
|
|
45
|
+
* @module lib/engine/tools/bash-background
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import { spawn } from "node:child_process";
|
|
49
|
+
import { randomBytes } from "node:crypto";
|
|
50
|
+
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readSync, statSync } from "node:fs";
|
|
51
|
+
import path from "node:path";
|
|
52
|
+
import { bashTool, scrubToolEnv } from "./bash.mjs";
|
|
53
|
+
import { createProcessTracker, newProcessTag, PROCESS_TAG_ENV } from "../process-tree.mjs";
|
|
54
|
+
|
|
55
|
+
export const BACKGROUND_OUTPUT_CAP = 30_000;
|
|
56
|
+
export const SPILL_FILE_CAP = 50 * 1024 * 1024;
|
|
57
|
+
const KILL_GRACE_MS = 2_000;
|
|
58
|
+
/** After SIGKILL, how long the pipes may take to close before they are destroyed. */
|
|
59
|
+
const PIPE_SETTLE_MS = 250;
|
|
60
|
+
|
|
61
|
+
/** @param {string} command @param {string} marker */
|
|
62
|
+
export function wrapWithCwdCapture(command, marker) {
|
|
63
|
+
return `${command}\n__cohort_status=$?\nprintf '\\n%s%s\\n' '${marker}' "$(pwd -P 2>/dev/null || pwd)"\nexit $__cohort_status`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Remove the capture line from a command's output.
|
|
68
|
+
* @param {string} text @param {string} marker
|
|
69
|
+
* @returns {{text:string, cwd:string|null}}
|
|
70
|
+
*/
|
|
71
|
+
export function splitCwdCapture(text, marker) {
|
|
72
|
+
const at = text.lastIndexOf(`\n${marker}`);
|
|
73
|
+
if (at === -1) return { text, cwd: null };
|
|
74
|
+
const end = text.indexOf("\n", at + 1 + marker.length);
|
|
75
|
+
const cwd = text.slice(at + 1 + marker.length, end === -1 ? text.length : end);
|
|
76
|
+
const rest = end === -1 ? "" : text.slice(end + 1);
|
|
77
|
+
return { text: text.slice(0, at) + (rest ? `\n${rest}` : ""), cwd: cwd.startsWith("/") ? cwd : null };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @param {string} text @param {RegExp} re */
|
|
81
|
+
export function filterLines(text, re) {
|
|
82
|
+
return text
|
|
83
|
+
.split("\n")
|
|
84
|
+
.filter((l) => re.test(l))
|
|
85
|
+
.join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @typedef {Object} ShellRecord
|
|
90
|
+
* @property {string} id
|
|
91
|
+
* @property {string} command
|
|
92
|
+
* @property {string|null} description
|
|
93
|
+
* @property {import('node:child_process').ChildProcess} child
|
|
94
|
+
* @property {'running'|'completed'|'failed'|'killed'} status
|
|
95
|
+
* @property {number|null} exitCode
|
|
96
|
+
* @property {string} unread in-memory output not yet returned
|
|
97
|
+
* @property {number} chars characters produced so far
|
|
98
|
+
* @property {string|null} spillPath
|
|
99
|
+
* @property {number} readBytes spill mode: bytes of the log already returned
|
|
100
|
+
* @property {number} spilledBytes spill mode: bytes written to the log
|
|
101
|
+
* @property {number} droppedBytes output not kept once the log reached its cap
|
|
102
|
+
* @property {Promise<void>} exited resolves when the pipes close (or are destroyed)
|
|
103
|
+
* @property {boolean} [processExited] bash itself has exited (its pipes may still be open)
|
|
104
|
+
* @property {boolean} [pipesClosed]
|
|
105
|
+
* @property {() => void} [release] end `exited` without the pipes closing
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* What a finished shell reports to `onExit` (W4-A1, CF-50). Pure.
|
|
110
|
+
* @param {ShellRecord} s
|
|
111
|
+
*/
|
|
112
|
+
export function shellSummary(s) {
|
|
113
|
+
return { id: s.id, command: s.command, description: s.description, kind: s.kind ?? "bash", status: s.status, exitCode: s.exitCode, spillPath: s.spillPath };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {{spillDir:string, env:NodeJS.ProcessEnv, cap?:number, spillFileCap?:number, killGraceMs?:number,
|
|
118
|
+
* onExit?:(summary:ReturnType<typeof shellSummary>)=>void}} o
|
|
119
|
+
* `onExit` (W4-A1, CF-50): called once when a shell ends on its own (completed or failed) —
|
|
120
|
+
* not when it was killed (KillShell, TaskStop, a monitor's timeout, the end of the run).
|
|
121
|
+
*/
|
|
122
|
+
export function createShellManager({ spillDir, env, cap = BACKGROUND_OUTPUT_CAP, spillFileCap = SPILL_FILE_CAP, killGraceMs = KILL_GRACE_MS, onExit: onShellExit, passthrough = [], processTracker }) {
|
|
123
|
+
/** @type {Map<string, ShellRecord>} */
|
|
124
|
+
const shells = new Map();
|
|
125
|
+
let seq = 0;
|
|
126
|
+
// CF-48: the processes each shell started, including those that called setsid (process-tree.mjs).
|
|
127
|
+
const tracker = processTracker ?? createProcessTracker();
|
|
128
|
+
|
|
129
|
+
const killGroup = (/** @type {ShellRecord} */ s, /** @type {NodeJS.Signals} */ sig) => {
|
|
130
|
+
try {
|
|
131
|
+
process.kill(-(/** @type number */ (s.child.pid)), sig);
|
|
132
|
+
} catch {
|
|
133
|
+
/* the group already exited */
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const groupAlive = (/** @type {ShellRecord} */ s) => {
|
|
137
|
+
try {
|
|
138
|
+
process.kill(-(/** @type number */ (s.child.pid)), 0);
|
|
139
|
+
return true;
|
|
140
|
+
} catch (e) {
|
|
141
|
+
return /** @type {NodeJS.ErrnoException} */ (e).code === "EPERM";
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const onExit = () => {
|
|
145
|
+
for (const s of shells.values()) if (s.status === "running") killGroup(s, "SIGKILL");
|
|
146
|
+
tracker.killSync([...shells.keys()]);
|
|
147
|
+
};
|
|
148
|
+
process.on("exit", onExit);
|
|
149
|
+
|
|
150
|
+
/** Write to the spill file up to its cap; what does not fit is counted, not kept. */
|
|
151
|
+
const spill = (/** @type {ShellRecord} */ s, /** @type string */ text) => {
|
|
152
|
+
const bytes = Buffer.byteLength(text);
|
|
153
|
+
const room = spillFileCap - s.spilledBytes;
|
|
154
|
+
if (bytes <= room) {
|
|
155
|
+
appendFileSync(/** @type string */ (s.spillPath), text);
|
|
156
|
+
s.spilledBytes += bytes;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (room > 0) {
|
|
160
|
+
// Cut at a line end inside the room, so the file never ends mid-character.
|
|
161
|
+
const buf = Buffer.from(text).subarray(0, room);
|
|
162
|
+
const nl = buf.lastIndexOf(0x0a);
|
|
163
|
+
const keep = nl === -1 ? 0 : nl + 1;
|
|
164
|
+
if (keep > 0) appendFileSync(/** @type string */ (s.spillPath), buf.subarray(0, keep));
|
|
165
|
+
s.spilledBytes += keep;
|
|
166
|
+
s.droppedBytes += bytes - keep;
|
|
167
|
+
if (keep < room) s.spilledBytes = spillFileCap; // the file is closed to further output
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
s.droppedBytes += bytes;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const append = (/** @type {ShellRecord} */ s, /** @type string */ chunk) => {
|
|
174
|
+
s.chars += chunk.length;
|
|
175
|
+
if (s.spillPath) {
|
|
176
|
+
spill(s, chunk);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
s.unread += chunk;
|
|
180
|
+
if (s.unread.length > cap) {
|
|
181
|
+
mkdirSync(spillDir, { recursive: true });
|
|
182
|
+
s.spillPath = path.join(spillDir, `${s.id}.log`);
|
|
183
|
+
const text = s.unread;
|
|
184
|
+
s.unread = "";
|
|
185
|
+
spill(s, text);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* CF-48: record every process these shells started (tree, samples, tag) and SIGTERM
|
|
191
|
+
* the ones outside the group; call before the group signal, so parents are still there.
|
|
192
|
+
* @param {string[]} ids
|
|
193
|
+
*/
|
|
194
|
+
const terminateTracked = (ids) => {
|
|
195
|
+
try {
|
|
196
|
+
tracker.capture(ids, { tags: true });
|
|
197
|
+
tracker.signal(ids, "SIGTERM");
|
|
198
|
+
} catch {
|
|
199
|
+
/* the group signal still goes out */
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Wait for killed shells: SIGKILL at the grace period, then at most a short
|
|
205
|
+
* settle for its pipes; a pipe still held open (by a process that left the
|
|
206
|
+
* group) is destroyed so the wait always ends. The tracked processes of
|
|
207
|
+
* `ids` (CF-48) get the rest of the grace period, then SIGKILL if their
|
|
208
|
+
* identity still matches.
|
|
209
|
+
* @param {ShellRecord[]} list running shells
|
|
210
|
+
* @param {string[]} [ids] every shell whose tracked processes are reaped
|
|
211
|
+
*/
|
|
212
|
+
const settle = async (list, ids = list.map((s) => s.id)) => {
|
|
213
|
+
const started = Date.now();
|
|
214
|
+
const reapTracked = async () => {
|
|
215
|
+
if (ids.length === 0) return;
|
|
216
|
+
try {
|
|
217
|
+
await tracker.reap(ids, killGraceMs - (Date.now() - started));
|
|
218
|
+
} catch {
|
|
219
|
+
/* bounded best effort */
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
if (list.length === 0) {
|
|
223
|
+
await reapTracked();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
/** @type {NodeJS.Timeout[]} */
|
|
227
|
+
const timers = [];
|
|
228
|
+
const closed = Promise.all(list.map((s) => s.exited));
|
|
229
|
+
const graceOver = new Promise((resolve) => {
|
|
230
|
+
timers.push(
|
|
231
|
+
setTimeout(() => {
|
|
232
|
+
for (const s of list) killGroup(s, "SIGKILL");
|
|
233
|
+
timers.push(setTimeout(resolve, PIPE_SETTLE_MS));
|
|
234
|
+
}, killGraceMs),
|
|
235
|
+
);
|
|
236
|
+
});
|
|
237
|
+
// Bash has exited and nothing is left in its group: only an escaped process holds the pipes.
|
|
238
|
+
const groupGone = new Promise((resolve) => {
|
|
239
|
+
const poll = setInterval(() => {
|
|
240
|
+
if (list.every((s) => s.pipesClosed || (s.processExited && !groupAlive(s)))) resolve(undefined);
|
|
241
|
+
}, 50);
|
|
242
|
+
timers.push(poll);
|
|
243
|
+
});
|
|
244
|
+
await Promise.race([closed, graceOver, groupGone]);
|
|
245
|
+
for (const t of timers) clearTimeout(t);
|
|
246
|
+
await reapTracked();
|
|
247
|
+
for (const s of list) {
|
|
248
|
+
if (s.pipesClosed) continue;
|
|
249
|
+
s.child.stdout?.destroy();
|
|
250
|
+
s.child.stderr?.destroy();
|
|
251
|
+
s.child.unref();
|
|
252
|
+
s.release?.();
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
/**
|
|
258
|
+
* @param {{command:string, cwd:string, description?:string|null, kind?:string, onLine?:(line:string)=>void}} p
|
|
259
|
+
* `onLine` (W4-A1, Monitor): each complete stdout line as it arrives (the output is still kept for BashOutput).
|
|
260
|
+
* @returns {{ok:true, id:string}|{ok:false, error:string}}
|
|
261
|
+
*/
|
|
262
|
+
start({ command, cwd, description = null, kind = "bash", onLine }) {
|
|
263
|
+
const id = `${kind === "bash" ? "bash" : kind}_${++seq}`;
|
|
264
|
+
const tag = newProcessTag();
|
|
265
|
+
let child;
|
|
266
|
+
try {
|
|
267
|
+
child = spawn("/bin/bash", ["-c", command], { cwd, env: { ...scrubToolEnv(env, { passthrough }), [PROCESS_TAG_ENV]: tag }, stdio: ["ignore", "pipe", "pipe"], detached: true });
|
|
268
|
+
} catch (e) {
|
|
269
|
+
return { ok: false, error: `Could not start bash: ${e instanceof Error ? e.message : e}` };
|
|
270
|
+
}
|
|
271
|
+
if (child.pid) tracker.track({ id, pid: child.pid, tag });
|
|
272
|
+
/** @type {ShellRecord} */
|
|
273
|
+
const s = { id, command, description, kind, child, status: "running", exitCode: null, unread: "", chars: 0, spillPath: null, readBytes: 0, spilledBytes: 0, droppedBytes: 0, exited: Promise.resolve() };
|
|
274
|
+
let lineBuf = "";
|
|
275
|
+
const emitLine = (/** @type string */ line) => {
|
|
276
|
+
try {
|
|
277
|
+
onLine?.(line);
|
|
278
|
+
} catch {
|
|
279
|
+
/* a line observer must not break the shell */
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
let reported = false;
|
|
283
|
+
const reportExit = () => {
|
|
284
|
+
if (reported) return; // 'error' can be followed by 'close'
|
|
285
|
+
reported = true;
|
|
286
|
+
if (lineBuf !== "") emitLine(lineBuf);
|
|
287
|
+
lineBuf = "";
|
|
288
|
+
if (s.status === "killed" || !onShellExit) return;
|
|
289
|
+
try {
|
|
290
|
+
onShellExit(shellSummary(s));
|
|
291
|
+
} catch {
|
|
292
|
+
/* a notification observer must not break the shell */
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
s.exited = new Promise((resolve) => {
|
|
296
|
+
s.release = () => resolve();
|
|
297
|
+
child.on("error", (e) => {
|
|
298
|
+
append(s, `bash error: ${e.message}\n`);
|
|
299
|
+
if (s.status === "running") s.status = "failed";
|
|
300
|
+
s.pipesClosed = true;
|
|
301
|
+
reportExit();
|
|
302
|
+
resolve();
|
|
303
|
+
});
|
|
304
|
+
// 'exit' records the code (the pipes may stay open); 'close' ends the wait.
|
|
305
|
+
child.on("exit", (code) => {
|
|
306
|
+
if (s.exitCode === null) s.exitCode = code;
|
|
307
|
+
s.processExited = true;
|
|
308
|
+
});
|
|
309
|
+
child.on("close", (code) => {
|
|
310
|
+
if (s.exitCode === null) s.exitCode = code;
|
|
311
|
+
if (s.status === "running") s.status = s.exitCode === 0 ? "completed" : "failed";
|
|
312
|
+
s.pipesClosed = true;
|
|
313
|
+
tracker.rootExited(id); // stop sampling; what was recorded is still reaped at killAll
|
|
314
|
+
reportExit();
|
|
315
|
+
resolve();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
child.stdout?.setEncoding("utf8");
|
|
319
|
+
child.stderr?.setEncoding("utf8");
|
|
320
|
+
child.stdout?.on("data", (d) => {
|
|
321
|
+
append(s, d);
|
|
322
|
+
if (!onLine) return;
|
|
323
|
+
lineBuf += d;
|
|
324
|
+
let nl;
|
|
325
|
+
while ((nl = lineBuf.indexOf("\n")) !== -1) {
|
|
326
|
+
emitLine(lineBuf.slice(0, nl).replace(/\r$/, ""));
|
|
327
|
+
lineBuf = lineBuf.slice(nl + 1);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
child.stderr?.on("data", (d) => append(s, d));
|
|
331
|
+
shells.set(id, s);
|
|
332
|
+
return { ok: true, id };
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
/** @param {string} id */
|
|
336
|
+
has: (id) => shells.has(id),
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* New output since the last read.
|
|
340
|
+
* @param {string} id @param {string|undefined} filter
|
|
341
|
+
* @returns {{ok:true, content:string, status:string}|{ok:false, error:string}}
|
|
342
|
+
*/
|
|
343
|
+
read(id, filter) {
|
|
344
|
+
const s = shells.get(id);
|
|
345
|
+
if (!s) return { ok: false, error: `There is no background shell "${id}". Known: ${[...shells.keys()].join(", ") || "none"}.` };
|
|
346
|
+
let re = null;
|
|
347
|
+
if (filter) {
|
|
348
|
+
try {
|
|
349
|
+
re = new RegExp(filter);
|
|
350
|
+
} catch (e) {
|
|
351
|
+
return { ok: false, error: `filter is not a valid regular expression: ${e instanceof Error ? e.message : e}` };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
let text;
|
|
355
|
+
let more = 0;
|
|
356
|
+
if (s.spillPath) {
|
|
357
|
+
const size = statSync(s.spillPath).size;
|
|
358
|
+
const want = Math.min(size - s.readBytes, cap);
|
|
359
|
+
const buf = Buffer.alloc(want);
|
|
360
|
+
const fd = openSync(s.spillPath, "r");
|
|
361
|
+
try {
|
|
362
|
+
readSync(fd, buf, 0, want, s.readBytes);
|
|
363
|
+
} finally {
|
|
364
|
+
closeSync(fd);
|
|
365
|
+
}
|
|
366
|
+
// Never split a UTF-8 character: stop at the last newline when cut short.
|
|
367
|
+
let take = buf.length;
|
|
368
|
+
if (s.readBytes + want < size) {
|
|
369
|
+
const nl = buf.lastIndexOf(0x0a);
|
|
370
|
+
if (nl > 0) take = nl + 1;
|
|
371
|
+
}
|
|
372
|
+
text = buf.subarray(0, take).toString("utf8");
|
|
373
|
+
s.readBytes += take;
|
|
374
|
+
more = size - s.readBytes;
|
|
375
|
+
} else {
|
|
376
|
+
text = s.unread;
|
|
377
|
+
s.unread = "";
|
|
378
|
+
}
|
|
379
|
+
if (re) text = filterLines(text, re);
|
|
380
|
+
const state = s.status === "running" ? "running" : s.status === "killed" ? "killed" : `exited with code ${s.exitCode}`;
|
|
381
|
+
const notes = [`[shell ${id}: ${state}]`];
|
|
382
|
+
if (s.spillPath) notes.push(`[the full log is in ${s.spillPath}${more > 0 ? `; ${more} more bytes are waiting — read again` : ""}]`);
|
|
383
|
+
if (s.droppedBytes > 0) notes.push(`[the log reached its ${spillFileCap} byte limit; ${s.droppedBytes} bytes of later output were not kept]`);
|
|
384
|
+
return { ok: true, status: s.status, content: `${text === "" ? "(no new output)" : text}\n\n${notes.join("\n")}` };
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Wait for a shell to exit, at most `ms`.
|
|
389
|
+
* @param {string} id @param {number} ms @param {AbortSignal} [signal]
|
|
390
|
+
*/
|
|
391
|
+
async wait(id, ms, signal) {
|
|
392
|
+
const s = shells.get(id);
|
|
393
|
+
if (!s || s.status !== "running") return;
|
|
394
|
+
await new Promise((resolve) => {
|
|
395
|
+
const t = setTimeout(done, ms);
|
|
396
|
+
const onAbort = () => done();
|
|
397
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
398
|
+
s.exited.then(done);
|
|
399
|
+
function done() {
|
|
400
|
+
clearTimeout(t);
|
|
401
|
+
signal?.removeEventListener("abort", onAbort);
|
|
402
|
+
resolve(undefined);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* @param {string} id
|
|
409
|
+
* @returns {Promise<{ok:true, content:string}|{ok:false, error:string}>}
|
|
410
|
+
*/
|
|
411
|
+
async kill(id) {
|
|
412
|
+
const s = shells.get(id);
|
|
413
|
+
if (!s) return { ok: false, error: `There is no background shell "${id}".` };
|
|
414
|
+
if (s.status !== "running") return { ok: true, content: `Shell ${id} had already ${s.status === "killed" ? "been killed" : `exited with code ${s.exitCode}`}.` };
|
|
415
|
+
s.status = "killed";
|
|
416
|
+
terminateTracked([id]);
|
|
417
|
+
killGroup(s, "SIGTERM");
|
|
418
|
+
await settle([s]);
|
|
419
|
+
return { ok: true, content: `Killed shell ${id} (${s.command.slice(0, 80)}).` };
|
|
420
|
+
},
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Kill every running group, and every tracked process of every shell this manager
|
|
424
|
+
* started — finished ones too (CF-48) — and wait for them; removes the exit listener.
|
|
425
|
+
*/
|
|
426
|
+
async killAll() {
|
|
427
|
+
const running = [...shells.values()].filter((s) => s.status === "running");
|
|
428
|
+
const ids = [...shells.keys()];
|
|
429
|
+
terminateTracked(ids);
|
|
430
|
+
for (const s of running) {
|
|
431
|
+
s.status = "killed";
|
|
432
|
+
killGroup(s, "SIGTERM");
|
|
433
|
+
}
|
|
434
|
+
await settle(running, ids);
|
|
435
|
+
tracker.dispose();
|
|
436
|
+
process.removeListener("exit", onExit);
|
|
437
|
+
},
|
|
438
|
+
|
|
439
|
+
/** CF-48: the tracker (tests and the session runtime read what a shell recorded). */
|
|
440
|
+
tracker,
|
|
441
|
+
|
|
442
|
+
list: () => [...shells.values()].map((s) => ({ id: s.id, command: s.command, status: s.status, exitCode: s.exitCode, kind: s.kind ?? "bash", description: s.description })),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* The session's Bash: bash.mjs's tool with `run_in_background` and a
|
|
448
|
+
* persistent working directory.
|
|
449
|
+
* @param {{shells:ReturnType<typeof createShellManager>, projectCwd:string}} o
|
|
450
|
+
*/
|
|
451
|
+
export function createSessionBash({ shells, projectCwd }) {
|
|
452
|
+
const marker = `__COHORT_CWD_${randomBytes(6).toString("hex")}__`;
|
|
453
|
+
const state = { cwd: projectCwd };
|
|
454
|
+
const tool = {
|
|
455
|
+
...bashTool,
|
|
456
|
+
description:
|
|
457
|
+
`${bashTool.description} The working directory carries over between calls (a \`cd\` holds for the next call). ` +
|
|
458
|
+
"Set `run_in_background` to start a long-running command and get a shell id back at once; read its output with BashOutput and stop it with KillShell.",
|
|
459
|
+
input_schema: {
|
|
460
|
+
...bashTool.input_schema,
|
|
461
|
+
properties: { ...bashTool.input_schema.properties, run_in_background: { type: "boolean", description: "Start the command in the background and return its shell id." } },
|
|
462
|
+
},
|
|
463
|
+
/**
|
|
464
|
+
* @param {{command:string, timeout?:number, description?:string, run_in_background?:boolean}} input
|
|
465
|
+
* @param {import('./index.mjs').ToolContext} ctx
|
|
466
|
+
*/
|
|
467
|
+
async run(input, ctx) {
|
|
468
|
+
let note = "";
|
|
469
|
+
if (!existsSync(state.cwd)) {
|
|
470
|
+
note = `\n[${state.cwd} no longer exists; the command ran in ${projectCwd}]`;
|
|
471
|
+
state.cwd = projectCwd;
|
|
472
|
+
}
|
|
473
|
+
if (input.run_in_background) {
|
|
474
|
+
const r = shells.start({ command: input.command, cwd: state.cwd, description: input.description ?? null });
|
|
475
|
+
if (!r.ok) return r;
|
|
476
|
+
return { ok: true, content: `Started in the background as shell ${r.id} (in ${state.cwd}). Read its output with BashOutput {"bash_id":"${r.id}"}; stop it with KillShell {"shell_id":"${r.id}"}.${note}` };
|
|
477
|
+
}
|
|
478
|
+
const { run_in_background: _bg, ...rest } = input;
|
|
479
|
+
const r = await bashTool.run({ ...rest, command: wrapWithCwdCapture(input.command, marker) }, { ...ctx, cwd: state.cwd });
|
|
480
|
+
const { text, cwd } = splitCwdCapture(r.ok ? r.content : r.error, marker);
|
|
481
|
+
if (cwd && cwd !== state.cwd && existsSync(cwd)) state.cwd = cwd;
|
|
482
|
+
if (r.ok) return { ok: true, content: (text === "" ? "(no output)" : text) + note };
|
|
483
|
+
return { ok: false, error: text + note };
|
|
484
|
+
},
|
|
485
|
+
/** A subagent's Bash: the same background shells, its own directory (a workflow child's worktree, W4-A2). */
|
|
486
|
+
forChild: (/** @type {{cwd?:string}|undefined} */ o) => createSessionBash({ shells, projectCwd: o?.cwd ?? projectCwd }),
|
|
487
|
+
get cwd() {
|
|
488
|
+
return state.cwd;
|
|
489
|
+
},
|
|
490
|
+
};
|
|
491
|
+
return tool;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** @param {ReturnType<typeof createShellManager>} shells */
|
|
495
|
+
export function createBashOutputTool(shells) {
|
|
496
|
+
return {
|
|
497
|
+
name: "BashOutput",
|
|
498
|
+
readOnly: true,
|
|
499
|
+
description:
|
|
500
|
+
"Read the output a background shell (started with Bash run_in_background) has produced since the last read, and whether it is still running. " +
|
|
501
|
+
"`filter` is an optional regular expression: only matching lines are returned, and the rest of the new output is consumed.",
|
|
502
|
+
input_schema: {
|
|
503
|
+
type: "object",
|
|
504
|
+
properties: {
|
|
505
|
+
bash_id: { type: "string", description: "The shell id Bash returned." },
|
|
506
|
+
filter: { type: "string", description: "A regular expression selecting the lines to return." },
|
|
507
|
+
},
|
|
508
|
+
required: ["bash_id"],
|
|
509
|
+
},
|
|
510
|
+
async run(/** @type {{bash_id:string, filter?:string}} */ input) {
|
|
511
|
+
const r = shells.read(input.bash_id, input.filter);
|
|
512
|
+
return r.ok ? { ok: true, content: r.content } : r;
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** @param {ReturnType<typeof createShellManager>} shells */
|
|
518
|
+
export function createKillShellTool(shells) {
|
|
519
|
+
return {
|
|
520
|
+
name: "KillShell",
|
|
521
|
+
// Stops only a process this run started itself.
|
|
522
|
+
readOnly: true,
|
|
523
|
+
description: "Stop a background shell started with Bash run_in_background, including every process it started.",
|
|
524
|
+
input_schema: {
|
|
525
|
+
type: "object",
|
|
526
|
+
properties: { shell_id: { type: "string", description: "The shell id Bash returned." } },
|
|
527
|
+
required: ["shell_id"],
|
|
528
|
+
},
|
|
529
|
+
async run(/** @type {{shell_id:string}} */ input) {
|
|
530
|
+
return shells.kill(input.shell_id);
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
}
|