@cohortapp/agent-sdk 2.16.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 +23 -7
- 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/poller-daemon-setup.md +4 -1
- 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/context/budget.mjs +327 -0
- package/lib/context/history-scope.mjs +138 -0
- 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/economics.mjs +9 -0
- package/lib/model-router/failover.mjs +67 -16
- package/lib/model-router/llm-task.mjs +39 -3
- package/lib/model-router/resolve.mjs +95 -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 +82 -4
- package/lib/org/inbound/hydrate.mjs +555 -51
- 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 +15 -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 +104 -59
- package/scripts/daemon/deliver.mjs +30 -1
- package/scripts/daemon/dispatcher.mjs +804 -157
- package/scripts/daemon/health.mjs +14 -1
- package/scripts/daemon/lib/session-router.mjs +310 -42
- package/scripts/daemon/maestro-daemon.mjs +11 -0
- package/scripts/daemon/prompt-builder.mjs +121 -12
- package/scripts/daemon/responder.mjs +315 -146
- 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/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 -453
- 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 -300
- 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/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 -295
- package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
- package/scripts/daemon/prompt-builder.test.mjs +0 -344
- package/scripts/daemon/responder-cost.test.mjs +0 -68
- package/scripts/daemon/responder-history.test.mjs +0 -185
- 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,1204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/cli.mjs — `cohort run`, the engine's headless entry point.
|
|
3
|
+
*
|
|
4
|
+
* cohort run -p "<prompt>" --output-format json --model cohort-agentic \
|
|
5
|
+
* --session-id <id> --max-turns 20 --mcp-config .mcp.json --strict-mcp-config \
|
|
6
|
+
* --permission-mode dontAsk --allowedTools "Read,Grep,mcp__cohort"
|
|
7
|
+
*
|
|
8
|
+
* Flags mirror the subset of the `claude --print` surface maestro's lanes use,
|
|
9
|
+
* so the runtime adapter (W1-E) can swap binaries by changing argv[0] and a
|
|
10
|
+
* handful of env names:
|
|
11
|
+
*
|
|
12
|
+
* -p, --print [prompt] headless (the only mode); the prompt may
|
|
13
|
+
* follow -p, be positional, or come on stdin
|
|
14
|
+
* --output-format text|json default text
|
|
15
|
+
* --model <tier> default $COHORT_LLM_MODEL or cohort-agentic
|
|
16
|
+
* --session-id <id> start a session with this id, or continue it when it exists
|
|
17
|
+
* and no live run holds it (one writer per session)
|
|
18
|
+
* --resume <id> continue an existing session
|
|
19
|
+
* --max-turns <n> default 50
|
|
20
|
+
* --append-system-prompt <s> appended to the system prompt
|
|
21
|
+
* --system-prompt <s> replaces the base system prompt
|
|
22
|
+
* --tools <list> built-in tools to expose ("" for none; default all)
|
|
23
|
+
* --base-url <url> default $COHORT_LLM_BASE_URL
|
|
24
|
+
* --wire openai|anthropic default $COHORT_LLM_WIRE or openai
|
|
25
|
+
* --max-output-tokens <n> per-turn output ceiling (default 16000)
|
|
26
|
+
* --wall-clock-ms <n> whole-run time limit (default 30 min)
|
|
27
|
+
* --mcp-config <file|json> MCP servers (repeatable)
|
|
28
|
+
* --strict-mcp-config only the --mcp-config servers
|
|
29
|
+
* --permission-mode <mode> default|acceptEdits|plan|dontAsk|bypassPermissions
|
|
30
|
+
* --dangerously-skip-permissions = --permission-mode bypassPermissions
|
|
31
|
+
* --allowedTools <rules> allow rules (repeatable; comma or space separated)
|
|
32
|
+
* --disallowedTools <rules> deny rules; a rule with no specifier also hides the tool
|
|
33
|
+
* --settings <file|json> a settings layer above local/project/user
|
|
34
|
+
* --add-dir <dir> an extra directory acceptEdits may write in (repeatable)
|
|
35
|
+
* --plugin-dir <dir> a plugin root whose skills and subagents load (repeatable)
|
|
36
|
+
* --output-format stream-json NDJSON events (output/stream-json.mjs)
|
|
37
|
+
* --input-format stream-json one turn per NDJSON user line on stdin, until EOF
|
|
38
|
+
* --include-partial-messages stream_event deltas (stream-json output only)
|
|
39
|
+
* --verbose debug notes on stderr
|
|
40
|
+
* --agents <json> inline subagent definitions (agents/definitions.mjs)
|
|
41
|
+
* --effort <level> reasoning effort where the tier takes it (wire/effort.mjs)
|
|
42
|
+
* --bare no user/project/local settings, hooks, instruction files,
|
|
43
|
+
* skills, subagents or project MCP servers; managed settings,
|
|
44
|
+
* --settings, --mcp-config, --plugin-dir and --agents still apply
|
|
45
|
+
*
|
|
46
|
+
* W3-G1 adds the per-run tools (tools/session.mjs: TodoWrite, subagents,
|
|
47
|
+
* background shells, NotebookEdit), subagent discovery and the agent runtime,
|
|
48
|
+
* per-tier usage aggregation into the result (`modelUsage`, `cohort.modelUsage`,
|
|
49
|
+
* `cohort.todos`, `cohort.agents`), and a turn loop so one session can take
|
|
50
|
+
* several stream-json prompts.
|
|
51
|
+
*
|
|
52
|
+
* `cohort auth status --json` (auth-status.mjs) proves the credential against
|
|
53
|
+
* GET /cohort/v1/quota for doctor and the setup probe.
|
|
54
|
+
*
|
|
55
|
+
* The gateway credential comes from the environment only — never a flag, so it
|
|
56
|
+
* never lands in a process listing: `$COHORT_LLM_TOKEN_HELPER` (a command
|
|
57
|
+
* re-run on a TTL and after a 401, wire/token-provider.mjs) or
|
|
58
|
+
* `$COHORT_LLM_TOKEN`.
|
|
59
|
+
*
|
|
60
|
+
* What a run assembles, in order: settings layers (permissions, hooks, plugin
|
|
61
|
+
* dirs) → MCP config → the session → instruction files and skills → MCP
|
|
62
|
+
* servers → tool list (built-ins, Skill, MCP; fully denied tools hidden) →
|
|
63
|
+
* SessionStart and UserPromptSubmit hooks → the loop, with every tool call
|
|
64
|
+
* passing the guard (permissions, in-process gates, PreToolUse hooks) and
|
|
65
|
+
* Stop hooks consulted before it ends. MCP servers are shut down however the
|
|
66
|
+
* run ends.
|
|
67
|
+
*
|
|
68
|
+
* `parseRunArgs` is pure; `runCli` is the edge and takes every effect as a
|
|
69
|
+
* dependency, so the end-to-end tests drive it in-process. The user-level
|
|
70
|
+
* layers (~/.claude/…) come from `deps.homedir`, else `env.HOME`; with neither
|
|
71
|
+
* there is no user layer.
|
|
72
|
+
*
|
|
73
|
+
* @module lib/engine/cli
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
import { randomUUID } from "node:crypto";
|
|
77
|
+
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
78
|
+
import os from "node:os";
|
|
79
|
+
import path from "node:path";
|
|
80
|
+
import { pathToFileURL } from "node:url";
|
|
81
|
+
import { runLoop, DEFAULTS } from "./loop.mjs";
|
|
82
|
+
import { createModelCaller, isWireName } from "./wire/index.mjs";
|
|
83
|
+
import { parseIdleTimeoutMs, idleTimeoutNote, formatStreamDiagnostic } from "./wire/stall.mjs";
|
|
84
|
+
import { selectTools, createToolContext } from "./tools/index.mjs";
|
|
85
|
+
import { openSession, appendMessage, appendResult, acquireSessionLock } from "./session/store.mjs";
|
|
86
|
+
import { createTokenProvider } from "./wire/token-provider.mjs";
|
|
87
|
+
import { runAuthStatus } from "./auth-status.mjs";
|
|
88
|
+
import { buildResult, buildSetupErrorResult } from "./output/json.mjs";
|
|
89
|
+
import { buildSystemPrompt } from "./prompt.mjs";
|
|
90
|
+
import { evaluatePermission, isPermissionMode, isToolFullyDenied, splitRuleList, PERMISSION_MODES } from "./permissions.mjs";
|
|
91
|
+
import { loadSettingsLayers, mergeSettings, defaultManagedSettingsPath, userConfigDir } from "./context/settings.mjs";
|
|
92
|
+
import { loadInstructions, formatInstructions, defaultManagedInstructionsPath } from "./context/instructions.mjs";
|
|
93
|
+
import { discoverSkills, formatSkillListing, createSkillTool } from "./skills/index.mjs";
|
|
94
|
+
import { readMcpConfigArg, loadMcpLayers, resolveMcpServers, filterProjectServers } from "./mcp/config.mjs";
|
|
95
|
+
import { startMcpServers } from "./mcp/index.mjs";
|
|
96
|
+
import { createHookRunner, createDefaultGates, SEND_GATE_NAME } from "./hooks.mjs";
|
|
97
|
+
import { createToolGuard } from "./guard.mjs";
|
|
98
|
+
import { createPathResolver } from "./context/real-path.mjs";
|
|
99
|
+
import { appendJsonl } from "../fs-atomic.mjs";
|
|
100
|
+
// W3-G1: subagents, session tools, stream-json, --effort.
|
|
101
|
+
import { parseAgentsJson, discoverAgents } from "./agents/definitions.mjs";
|
|
102
|
+
import { createAgentRuntime, subagentPrompt, DEFAULT_MAX_AGENT_DEPTH } from "./agents/runtime.mjs";
|
|
103
|
+
import { createWorkflowRuntime, workflowTimingFromEnv } from "./workflow/runtime.mjs";
|
|
104
|
+
import { mergeNotificationSources } from "./workflow/notifications.mjs";
|
|
105
|
+
import { createWorkflowTools } from "./tools/workflow.mjs";
|
|
106
|
+
import { aggregateUsage } from "./agents/usage.mjs";
|
|
107
|
+
import { createSessionToolkit, wantedSessionTools, SESSION_TOOL_NAMES } from "./tools/session.mjs";
|
|
108
|
+
import { createTodoState, loadSessionTodos } from "./tools/todo.mjs";
|
|
109
|
+
import { createStreamJsonWriter, observeQuota, parseInputLine } from "./output/stream-json.mjs";
|
|
110
|
+
import { EFFORT_LEVELS, isEffortLevel, resolveEffort } from "./wire/effort.mjs";
|
|
111
|
+
// W3-G2: context management, prompt caching, deferred tools, web tools, MCP resources, CF-21.
|
|
112
|
+
import { createContextManager } from "./context/manager.mjs";
|
|
113
|
+
import { resolveContextConfig } from "./context/budget.mjs";
|
|
114
|
+
import { stableToolOrder, promptCacheKey } from "./context/cache.mjs";
|
|
115
|
+
import { resumeHistory } from "./context/compaction.mjs";
|
|
116
|
+
import { createLazyInstructions } from "./context/lazy-instructions.mjs";
|
|
117
|
+
import { createStreamInput, parseStreamInputLine } from "./context/stream-input.mjs";
|
|
118
|
+
import { isCoreCredentialEnvName, parseEnvNameList, parsePassthroughEnv, PROJECT_SCOPES, withheldByValue } from "./context/child-env.mjs";
|
|
119
|
+
import { createDeferredTools, createToolSearchTool, shouldDeferTools, formatDeferredNote } from "./tools/toolsearch.mjs";
|
|
120
|
+
import { createMcpResourceTools } from "./mcp/resources.mjs";
|
|
121
|
+
import { transcriptMessage } from "./context/images.mjs";
|
|
122
|
+
import { webToolsEnabled, WEB_TOOL_NAMES } from "./tools/web-switch.mjs";
|
|
123
|
+
import { createPromptCacheKeyState, promptCacheKeyMode } from "./wire/prompt-cache.mjs";
|
|
124
|
+
// W4-A1: background-shell completion notices (CF-50); `cohort session` plugs in through `deps.host`.
|
|
125
|
+
import { createNotificationQueue, shellExitNotice } from "./session-runtime/notifications.mjs";
|
|
126
|
+
// W4-E1: --max-budget-usd.
|
|
127
|
+
import { parseBudgetUsd, createBudgetMeter } from "./budget.mjs";
|
|
128
|
+
// W4-E2: slash commands and skills invoked from a prompt (rows 14, 34).
|
|
129
|
+
import { discoverCommands, expandSlashCommand, resolveSlashCommand } from "./commands/index.mjs";
|
|
130
|
+
import { resolveAgentModel } from "./agents/definitions.mjs";
|
|
131
|
+
|
|
132
|
+
export const DEFAULT_MODEL = "cohort-agentic";
|
|
133
|
+
export const DEFAULT_MAX_OUTPUT_TOKENS = 16_000;
|
|
134
|
+
|
|
135
|
+
export const USAGE = `Usage: cohort run [-p] "<prompt>" [flags]
|
|
136
|
+
cohort auth status [--json] [--base-url <url>] does the gateway credential work?
|
|
137
|
+
|
|
138
|
+
Run the Cohort Engine headless: the prompt is worked to completion with the
|
|
139
|
+
built-in tools, MCP tools and skills, and the answer is printed.
|
|
140
|
+
|
|
141
|
+
Flags:
|
|
142
|
+
-p, --print [prompt] Headless mode; the prompt may follow -p
|
|
143
|
+
--output-format text|json Output format (default text)
|
|
144
|
+
--model <tier> Model tier (default $COHORT_LLM_MODEL or ${DEFAULT_MODEL})
|
|
145
|
+
--session-id <id> Start a session with this id, or continue it if it exists
|
|
146
|
+
--resume <id> Continue an existing session
|
|
147
|
+
--max-turns <n> Maximum model turns (default ${DEFAULTS.maxTurns})
|
|
148
|
+
--append-system-prompt <s> Text appended to the system prompt
|
|
149
|
+
--system-prompt <s> Replace the base system prompt
|
|
150
|
+
--tools <list> Built-in tools to expose, comma-separated ("" for none)
|
|
151
|
+
--base-url <url> Gateway URL (default $COHORT_LLM_BASE_URL)
|
|
152
|
+
--wire openai|anthropic Gateway wire (default $COHORT_LLM_WIRE or openai)
|
|
153
|
+
--max-output-tokens <n> Output tokens per turn (default ${DEFAULT_MAX_OUTPUT_TOKENS})
|
|
154
|
+
--wall-clock-ms <n> Time limit for the whole run (default ${DEFAULTS.wallClockMs})
|
|
155
|
+
--mcp-config <file|json> MCP servers to connect (repeatable)
|
|
156
|
+
--strict-mcp-config Use only --mcp-config servers
|
|
157
|
+
--permission-mode <mode> ${PERMISSION_MODES.join("|")}
|
|
158
|
+
--dangerously-skip-permissions Same as --permission-mode bypassPermissions
|
|
159
|
+
--allowedTools <rules> Allow rules, e.g. "Read,Bash(npm run test:*),mcp__cohort"
|
|
160
|
+
--disallowedTools <rules> Deny rules
|
|
161
|
+
--settings <file|json> Extra settings layer
|
|
162
|
+
--add-dir <dir> Extra directory edits are accepted in (repeatable)
|
|
163
|
+
--plugin-dir <dir> Plugin root to load skills and subagents from (repeatable)
|
|
164
|
+
--output-format stream-json NDJSON events instead of one result
|
|
165
|
+
--input-format text|stream-json stream-json (needs stream-json output): user turns as
|
|
166
|
+
NDJSON on stdin, and {"type":"control","subtype":"compact"}
|
|
167
|
+
--include-partial-messages With stream-json output: stream deltas
|
|
168
|
+
--verbose Print debug notes on stderr
|
|
169
|
+
--agents <json> Subagent definitions: {"name":{"description","prompt","tools","model"}}
|
|
170
|
+
--effort <level> ${EFFORT_LEVELS.join("|")}: reasoning effort where the tier takes it
|
|
171
|
+
--bare No user/project settings, hooks, instructions, skills, subagents or
|
|
172
|
+
project MCP; managed policy, --settings and --mcp-config still apply
|
|
173
|
+
--max-budget-usd <usd> Stop before the next model call once the run (subagents included) has
|
|
174
|
+
spent this much, by the gateway's cost figures (error_max_budget_usd)
|
|
175
|
+
|
|
176
|
+
Environment:
|
|
177
|
+
COHORT_LLM_TOKEN Gateway credential (this or COHORT_LLM_TOKEN_HELPER)
|
|
178
|
+
COHORT_LLM_TOKEN_HELPER Command printing a fresh credential; re-run on a TTL and after a 401
|
|
179
|
+
COHORT_LLM_TOKEN_TTL_MS How long a helper credential is reused (default 600000)
|
|
180
|
+
COHORT_LLM_BASE_URL Gateway URL
|
|
181
|
+
COHORT_ENGINE_SESSIONS_DIR Where transcripts are kept (default ~/.cohort/engine/sessions)
|
|
182
|
+
COHORT_ENGINE_CONTEXT_WINDOW Context window (tokens) to plan against
|
|
183
|
+
COHORT_ENGINE_COMPACT_THRESHOLD Compact at this share of the window (default 0.85)
|
|
184
|
+
COHORT_ENGINE_WEB_TOOLS 0 drops WebFetch and WebSearch from the default tools
|
|
185
|
+
COHORT_ENGINE_AUTO_COMPACT 0 turns automatic compaction off
|
|
186
|
+
COHORT_ENGINE_IMAGE_INPUT 1/0: send tool-result images to the model
|
|
187
|
+
COHORT_LLM_PROMPT_CACHE_KEY 1/0: send prompt_cache_key (default: when the gateway says so)
|
|
188
|
+
COHORT_LLM_STREAM_IDLE_TIMEOUT_MS End a gateway stream that sends no event for this long (default 60000; 0 disables; clamped to 5000-120000)
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
const VALUE_FLAGS = new Map([
|
|
192
|
+
["--output-format", "outputFormat"],
|
|
193
|
+
["--model", "model"],
|
|
194
|
+
["--session-id", "sessionId"],
|
|
195
|
+
["--resume", "resume"],
|
|
196
|
+
["-r", "resume"],
|
|
197
|
+
["--max-turns", "maxTurns"],
|
|
198
|
+
["--append-system-prompt", "appendSystemPrompt"],
|
|
199
|
+
["--system-prompt", "systemPrompt"],
|
|
200
|
+
["--tools", "tools"],
|
|
201
|
+
["--base-url", "baseUrl"],
|
|
202
|
+
["--wire", "wire"],
|
|
203
|
+
["--max-output-tokens", "maxTokens"],
|
|
204
|
+
["--wall-clock-ms", "wallClockMs"],
|
|
205
|
+
["--permission-mode", "permissionMode"],
|
|
206
|
+
["--settings", "settings"],
|
|
207
|
+
["--input-format", "inputFormat"],
|
|
208
|
+
["--effort", "effort"],
|
|
209
|
+
["--agents", "agents"],
|
|
210
|
+
// W4-E1 (row 24): a hard spend cap on the run, from the gateway's cost figures.
|
|
211
|
+
["--max-budget-usd", "maxBudgetUsd"],
|
|
212
|
+
]);
|
|
213
|
+
/** Flags that may repeat; values accumulate in order. */
|
|
214
|
+
const LIST_FLAGS = new Map([
|
|
215
|
+
["--mcp-config", "mcpConfig"],
|
|
216
|
+
["--allowedTools", "allowedTools"],
|
|
217
|
+
["--allowed-tools", "allowedTools"],
|
|
218
|
+
["--disallowedTools", "disallowedTools"],
|
|
219
|
+
["--disallowed-tools", "disallowedTools"],
|
|
220
|
+
["--add-dir", "addDirs"],
|
|
221
|
+
["--plugin-dir", "pluginDirs"],
|
|
222
|
+
]);
|
|
223
|
+
const BOOL_FLAGS = new Map([
|
|
224
|
+
["--strict-mcp-config", "strictMcpConfig"],
|
|
225
|
+
["--dangerously-skip-permissions", "dangerouslySkipPermissions"],
|
|
226
|
+
["--bare", "bare"],
|
|
227
|
+
["--verbose", "verbose"],
|
|
228
|
+
["--include-partial-messages", "includePartialMessages"],
|
|
229
|
+
]);
|
|
230
|
+
/**
|
|
231
|
+
* Every flag spelling `parseRunArgs` recognises, derived from the tables above
|
|
232
|
+
* (plus the print/help spellings it handles inline). The runtime adapter's
|
|
233
|
+
* drift test reads this instead of a hand-kept copy, so a flag the adapter
|
|
234
|
+
* emits for engine cohort that the parser does not know fails the build.
|
|
235
|
+
*/
|
|
236
|
+
export const RUN_FLAGS = Object.freeze([
|
|
237
|
+
"-p", "--print", "-h", "--help",
|
|
238
|
+
...VALUE_FLAGS.keys(), ...LIST_FLAGS.keys(), ...BOOL_FLAGS.keys(),
|
|
239
|
+
]);
|
|
240
|
+
const INT_FLAGS = new Set(["maxTurns", "maxTokens", "wallClockMs"]);
|
|
241
|
+
const RULE_LISTS = new Set(["allowedTools", "disallowedTools"]);
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @param {string[]} argv arguments after `run` (a leading `run` is tolerated)
|
|
245
|
+
* @returns {{ ok:true, opts: Record<string, any> } | { ok:false, error:string }}
|
|
246
|
+
*/
|
|
247
|
+
export function parseRunArgs(argv) {
|
|
248
|
+
const args = [...argv];
|
|
249
|
+
if (args[0] === "run") args.shift();
|
|
250
|
+
/** @type {Record<string, any>} */
|
|
251
|
+
const opts = {
|
|
252
|
+
outputFormat: "text",
|
|
253
|
+
inputFormat: "text",
|
|
254
|
+
print: false,
|
|
255
|
+
help: false,
|
|
256
|
+
prompt: null,
|
|
257
|
+
tools: null,
|
|
258
|
+
mcpConfig: [],
|
|
259
|
+
allowedTools: [],
|
|
260
|
+
disallowedTools: [],
|
|
261
|
+
addDirs: [],
|
|
262
|
+
pluginDirs: [],
|
|
263
|
+
strictMcpConfig: false,
|
|
264
|
+
dangerouslySkipPermissions: false,
|
|
265
|
+
bare: false,
|
|
266
|
+
verbose: false,
|
|
267
|
+
includePartialMessages: false,
|
|
268
|
+
};
|
|
269
|
+
const positional = [];
|
|
270
|
+
for (let i = 0; i < args.length; i++) {
|
|
271
|
+
let a = args[i];
|
|
272
|
+
let inline = null;
|
|
273
|
+
if (a.startsWith("--") && a.includes("=")) {
|
|
274
|
+
inline = a.slice(a.indexOf("=") + 1);
|
|
275
|
+
a = a.slice(0, a.indexOf("="));
|
|
276
|
+
}
|
|
277
|
+
if (a === "-h" || a === "--help") {
|
|
278
|
+
opts.help = true;
|
|
279
|
+
} else if (a === "-p" || a === "--print") {
|
|
280
|
+
opts.print = true;
|
|
281
|
+
const next = args[i + 1];
|
|
282
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
283
|
+
opts.prompt = next;
|
|
284
|
+
i++;
|
|
285
|
+
}
|
|
286
|
+
} else if (BOOL_FLAGS.has(a)) {
|
|
287
|
+
opts[/** @type string */ (BOOL_FLAGS.get(a))] = true;
|
|
288
|
+
} else if (VALUE_FLAGS.has(a) || LIST_FLAGS.has(a)) {
|
|
289
|
+
const key = /** @type string */ (VALUE_FLAGS.get(a) ?? LIST_FLAGS.get(a));
|
|
290
|
+
const value = inline ?? args[++i];
|
|
291
|
+
if (value === undefined) return { ok: false, error: `${a} needs a value` };
|
|
292
|
+
if (INT_FLAGS.has(key)) {
|
|
293
|
+
const n = Number(value);
|
|
294
|
+
if (!Number.isInteger(n) || n < 1) return { ok: false, error: `${a} must be a positive integer, got "${value}"` };
|
|
295
|
+
opts[key] = n;
|
|
296
|
+
} else if (RULE_LISTS.has(key)) {
|
|
297
|
+
opts[key].push(...splitRuleList(value));
|
|
298
|
+
} else if (LIST_FLAGS.has(a)) {
|
|
299
|
+
opts[key].push(value);
|
|
300
|
+
} else {
|
|
301
|
+
opts[key] = value;
|
|
302
|
+
}
|
|
303
|
+
} else if (a === "--") {
|
|
304
|
+
positional.push(...args.slice(i + 1));
|
|
305
|
+
break;
|
|
306
|
+
} else if (a.startsWith("-") && a !== "-") {
|
|
307
|
+
return { ok: false, error: `unknown flag ${a}` };
|
|
308
|
+
} else {
|
|
309
|
+
positional.push(a);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (opts.prompt === null && positional.length > 0) opts.prompt = positional.join(" ");
|
|
313
|
+
if (!["text", "json", "stream-json"].includes(opts.outputFormat)) {
|
|
314
|
+
return { ok: false, error: `--output-format must be text, json or stream-json` };
|
|
315
|
+
}
|
|
316
|
+
if (opts.inputFormat !== "text" && opts.inputFormat !== "stream-json") return { ok: false, error: "--input-format must be text or stream-json" };
|
|
317
|
+
if (opts.inputFormat === "stream-json" && opts.outputFormat !== "stream-json") return { ok: false, error: "--input-format stream-json needs --output-format stream-json" };
|
|
318
|
+
if (opts.includePartialMessages && opts.outputFormat !== "stream-json") return { ok: false, error: "--include-partial-messages needs --output-format stream-json" };
|
|
319
|
+
if (opts.effort !== undefined && !isEffortLevel(opts.effort)) return { ok: false, error: `--effort must be one of ${EFFORT_LEVELS.join(", ")}` };
|
|
320
|
+
if (opts.agents !== undefined) {
|
|
321
|
+
const parsedAgents = parseAgentsJson(opts.agents);
|
|
322
|
+
if (!parsedAgents.ok) return { ok: false, error: parsedAgents.error };
|
|
323
|
+
opts.agents = parsedAgents.agents;
|
|
324
|
+
} else {
|
|
325
|
+
opts.agents = [];
|
|
326
|
+
}
|
|
327
|
+
if (opts.maxBudgetUsd !== undefined) {
|
|
328
|
+
const b = parseBudgetUsd(opts.maxBudgetUsd);
|
|
329
|
+
if (!b.ok) return { ok: false, error: b.error };
|
|
330
|
+
opts.maxBudgetUsd = b.usd;
|
|
331
|
+
opts.maxBudgetMicros = b.micros;
|
|
332
|
+
}
|
|
333
|
+
if (opts.wire !== undefined && !isWireName(opts.wire)) return { ok: false, error: `--wire must be openai or anthropic` };
|
|
334
|
+
if (opts.sessionId && opts.resume && opts.sessionId !== opts.resume) {
|
|
335
|
+
return { ok: false, error: "--session-id and --resume name different sessions; pass one" };
|
|
336
|
+
}
|
|
337
|
+
if (opts.permissionMode !== undefined && !isPermissionMode(opts.permissionMode)) {
|
|
338
|
+
return { ok: false, error: `--permission-mode must be one of ${PERMISSION_MODES.join(", ")}` };
|
|
339
|
+
}
|
|
340
|
+
if (opts.dangerouslySkipPermissions && opts.permissionMode !== undefined && opts.permissionMode !== "bypassPermissions") {
|
|
341
|
+
return { ok: false, error: `--dangerously-skip-permissions conflicts with --permission-mode ${opts.permissionMode}` };
|
|
342
|
+
}
|
|
343
|
+
if (typeof opts.tools === "string") {
|
|
344
|
+
opts.tools = opts.tools.trim() === "" ? [] : opts.tools.split(/[,\s]+/).filter(Boolean);
|
|
345
|
+
}
|
|
346
|
+
return { ok: true, opts };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Did argv ask for JSON output? Read from the raw argv so it still answers when
|
|
351
|
+
* `parseRunArgs` refused some other flag.
|
|
352
|
+
* @param {string[]} argv
|
|
353
|
+
*/
|
|
354
|
+
export function wantsJson(argv) {
|
|
355
|
+
const structured = (/** @type {string|undefined} */ v) => v === "json" || v === "stream-json";
|
|
356
|
+
return argv.some((a, i) => (a.startsWith("--output-format=") && structured(a.slice("--output-format=".length))) || (a === "--output-format" && structured(argv[i + 1])));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @typedef {Object} CliFs
|
|
361
|
+
* @property {(p:string)=>string} readFile
|
|
362
|
+
* @property {(p:string)=>boolean} exists
|
|
363
|
+
* @property {(p:string)=>boolean} isFile
|
|
364
|
+
* @property {(p:string)=>boolean} isDir
|
|
365
|
+
* @property {(p:string)=>string[]} readdir
|
|
366
|
+
* @property {(p:string)=>string} realpath
|
|
367
|
+
*/
|
|
368
|
+
|
|
369
|
+
/** @type {CliFs} */
|
|
370
|
+
export const NODE_FS = Object.freeze({
|
|
371
|
+
readFile: (p) => readFileSync(p, "utf8"),
|
|
372
|
+
exists: (p) => existsSync(p),
|
|
373
|
+
isFile: (p) => {
|
|
374
|
+
try {
|
|
375
|
+
return statSync(p).isFile();
|
|
376
|
+
} catch {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
isDir: (p) => {
|
|
381
|
+
try {
|
|
382
|
+
return statSync(p).isDirectory();
|
|
383
|
+
} catch {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
readdir: (p) => readdirSync(p),
|
|
388
|
+
realpath: (p) => realpathSync(p),
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* @typedef {Object} CliDeps
|
|
393
|
+
* @property {NodeJS.ProcessEnv} [env]
|
|
394
|
+
* @property {string} [cwd]
|
|
395
|
+
* @property {{ write: (s:string) => unknown }} [stdout]
|
|
396
|
+
* @property {{ write: (s:string) => unknown }} [stderr]
|
|
397
|
+
* @property {typeof fetch} [fetchImpl] the gateway's fetch
|
|
398
|
+
* @property {typeof fetch} [mcpFetchImpl] MCP streamable HTTP's fetch
|
|
399
|
+
* @property {() => number} [now]
|
|
400
|
+
* @property {() => string} [newId]
|
|
401
|
+
* @property {() => Promise<string>} [readStdin]
|
|
402
|
+
* @property {string|null} [homedir] user layers (~/.claude); default env.HOME
|
|
403
|
+
* @property {string|null} [managedSettingsPath]
|
|
404
|
+
* @property {string|null} [managedInstructionsPath]
|
|
405
|
+
* @property {CliFs} [fs]
|
|
406
|
+
* @property {AbortSignal} [signal]
|
|
407
|
+
* @property {boolean} [useRipgrep]
|
|
408
|
+
* @property {(ms:number, s?:AbortSignal)=>Promise<void>} [sleep]
|
|
409
|
+
* @property {(command:string, o:{env:Record<string,string|undefined>}) => Promise<string>} [runTokenHelper] runs COHORT_LLM_TOKEN_HELPER (tests)
|
|
410
|
+
* @property {number} [pid] the session lock's holder pid (default process.pid; tests)
|
|
411
|
+
* @property {(pid:number) => boolean} [isProcessAlive] whether a session lock's holder is alive (tests)
|
|
412
|
+
* @property {(pid:number) => string|null} [processStartTime] a process's start time, for the session lock's pid-reuse check (tests)
|
|
413
|
+
* @property {AsyncIterable<Buffer|string> & {destroy?:()=>void}} [stdinStream] --input-format stream-json source (bytes)
|
|
414
|
+
* @property {AsyncIterable<string>|Iterable<string>} [stdinLines] --input-format stream-json source, one line per item (tests)
|
|
415
|
+
* @property {{allowHttp?:boolean, resolve?:Function, policy?:Function, cache?:any, now?:()=>number}} [web] WebFetch network options (tests)
|
|
416
|
+
* @property {ReturnType<typeof import('./session-runtime/host.mjs').createSessionHost>} [host]
|
|
417
|
+
* W4-A1 `cohort session`: its event queue, front-door tools, per-turn signals and approvals
|
|
418
|
+
*/
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* @param {string[]} argv
|
|
422
|
+
* @param {CliDeps} [deps]
|
|
423
|
+
* @returns {Promise<number>} process exit code
|
|
424
|
+
*/
|
|
425
|
+
export async function runCli(argv, deps = {}) {
|
|
426
|
+
// `cli.mjs auth status --json`: does this process's gateway credential work (rows 1 and 25)?
|
|
427
|
+
if (argv[0] === "auth") return runAuthStatus(argv.slice(1), deps);
|
|
428
|
+
/** Inputs released however the run ends (stream-json stdin). @type {Array<{close:()=>void}>} */
|
|
429
|
+
const inputs = [];
|
|
430
|
+
try {
|
|
431
|
+
return await runCliBody(argv, deps, inputs);
|
|
432
|
+
} finally {
|
|
433
|
+
for (const i of inputs) i.close();
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** @param {string[]} argv @param {CliDeps} deps @param {Array<{close:()=>void}>} inputs */
|
|
438
|
+
async function runCliBody(argv, deps, inputs) {
|
|
439
|
+
const env = deps.env ?? process.env;
|
|
440
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
441
|
+
const stdout = deps.stdout ?? process.stdout;
|
|
442
|
+
const stderr = deps.stderr ?? process.stderr;
|
|
443
|
+
const now = deps.now ?? Date.now;
|
|
444
|
+
const fs = deps.fs ?? NODE_FS;
|
|
445
|
+
const started = now();
|
|
446
|
+
const warn = (/** @type string */ line) => stderr.write(`cohort run: ${line}\n`);
|
|
447
|
+
|
|
448
|
+
const parsed = parseRunArgs(argv);
|
|
449
|
+
if (!parsed.ok) {
|
|
450
|
+
// A JSON caller parses stdout; it gets a result object even for a bad flag.
|
|
451
|
+
if (wantsJson(argv)) {
|
|
452
|
+
const wire = isWireName(env.COHORT_LLM_WIRE) ? env.COHORT_LLM_WIRE : "openai";
|
|
453
|
+
const r = buildSetupErrorResult({ sessionId: null, code: "invalid_arguments", message: parsed.error, durationMs: now() - started, wire });
|
|
454
|
+
stdout.write(JSON.stringify(r) + "\n");
|
|
455
|
+
} else {
|
|
456
|
+
stderr.write(`cohort run: ${parsed.error}\n\n${USAGE}`);
|
|
457
|
+
}
|
|
458
|
+
return 2;
|
|
459
|
+
}
|
|
460
|
+
const o = parsed.opts;
|
|
461
|
+
if (o.help) {
|
|
462
|
+
stdout.write(USAGE);
|
|
463
|
+
return 0;
|
|
464
|
+
}
|
|
465
|
+
const wire = o.wire ?? (isWireName(env.COHORT_LLM_WIRE) ? env.COHORT_LLM_WIRE : "openai");
|
|
466
|
+
|
|
467
|
+
/** @param {string} code @param {string} message @param {string|null} sessionId */
|
|
468
|
+
const setupFail = (code, message, sessionId = null) => {
|
|
469
|
+
if (o.outputFormat !== "text") {
|
|
470
|
+
const r = buildSetupErrorResult({ sessionId, code, message, durationMs: now() - started, wire });
|
|
471
|
+
stdout.write(JSON.stringify(r) + "\n");
|
|
472
|
+
} else {
|
|
473
|
+
stderr.write(`cohort run: ${message}\n`);
|
|
474
|
+
}
|
|
475
|
+
return 1;
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const baseUrl = o.baseUrl ?? env.COHORT_LLM_BASE_URL;
|
|
479
|
+
if (!baseUrl) return setupFail("missing_base_url", "no gateway URL: pass --base-url or set COHORT_LLM_BASE_URL");
|
|
480
|
+
// The credential may outlive a seat token (a `cohort session` runs for days): a helper
|
|
481
|
+
// command re-mints it on a TTL and after a 401; a bare COHORT_LLM_TOKEN is used as given.
|
|
482
|
+
const tokenProvider = createTokenProvider({ env, now, ...(deps.runTokenHelper ? { runHelper: deps.runTokenHelper } : {}) });
|
|
483
|
+
if (!tokenProvider.ok) return setupFail(tokenProvider.error.code, tokenProvider.error.message);
|
|
484
|
+
const token = tokenProvider.token;
|
|
485
|
+
|
|
486
|
+
// --input-format stream-json: turns arrive as NDJSON on stdin (a -p prompt, if any, is the first).
|
|
487
|
+
const streamInputWanted = o.inputFormat === "stream-json";
|
|
488
|
+
let prompt = o.prompt;
|
|
489
|
+
if (streamInputWanted && prompt === "-") prompt = null;
|
|
490
|
+
if (!streamInputWanted && (prompt === null || prompt === "-") && deps.readStdin) prompt = (await deps.readStdin()).trim();
|
|
491
|
+
if (!prompt && !streamInputWanted) return setupFail("missing_prompt", "no prompt: pass it after -p, as an argument, or on stdin");
|
|
492
|
+
|
|
493
|
+
const wantSkill = o.tools == null || o.tools.includes("Skill");
|
|
494
|
+
const hostToolNames = deps.host?.toolNames ?? [];
|
|
495
|
+
const { tools: builtins, unknown } = selectTools(o.tools == null ? null : o.tools.filter((/** @type string */ n) => n !== "Skill" && !SESSION_TOOL_NAMES.includes(n) && !hostToolNames.includes(n)));
|
|
496
|
+
if (unknown.length > 0) return setupFail("unknown_tool", `unknown tool(s) in --tools: ${unknown.join(", ")}`);
|
|
497
|
+
|
|
498
|
+
// ── settings: permissions, hooks, plugin dirs ──────────────────────────────
|
|
499
|
+
const homedir = deps.homedir !== undefined ? deps.homedir : env.HOME || null;
|
|
500
|
+
const userDir = userConfigDir({ homedir, cwd, env });
|
|
501
|
+
const platform = os.platform();
|
|
502
|
+
const loadedSettings = loadSettingsLayers({
|
|
503
|
+
homedir,
|
|
504
|
+
cwd,
|
|
505
|
+
env,
|
|
506
|
+
managedPath: deps.managedSettingsPath !== undefined ? deps.managedSettingsPath : defaultManagedSettingsPath(platform),
|
|
507
|
+
cliSettings: o.settings ?? null,
|
|
508
|
+
readFile: fs.readFile,
|
|
509
|
+
exists: fs.exists,
|
|
510
|
+
});
|
|
511
|
+
// --bare: of the settings files, only managed policy and an explicit --settings apply.
|
|
512
|
+
const settingsLayers = o.bare ? loadedSettings.layers.filter((l) => l.scope === "managed" || l.scope === "cli") : loadedSettings.layers;
|
|
513
|
+
const settings = mergeSettings(settingsLayers, {
|
|
514
|
+
allowedTools: o.allowedTools,
|
|
515
|
+
disallowedTools: o.disallowedTools,
|
|
516
|
+
permissionMode: o.dangerouslySkipPermissions ? "bypassPermissions" : o.permissionMode ?? null,
|
|
517
|
+
cwd,
|
|
518
|
+
});
|
|
519
|
+
// A settings problem that could drop a deny rule or a hook stops the run.
|
|
520
|
+
const settingsErrors = [...(o.bare ? loadedSettings.errors.filter((e) => /^(managed settings|cli settings|--settings)/.test(e)) : loadedSettings.errors), ...settings.errors];
|
|
521
|
+
if (settingsErrors.length > 0) return setupFail("invalid_settings", `settings could not be applied: ${settingsErrors.join("; ")}`);
|
|
522
|
+
for (const w of settings.warnings) warn(w);
|
|
523
|
+
const mode = settings.mode;
|
|
524
|
+
if (!isPermissionMode(mode)) return setupFail("invalid_settings", `permissions.defaultMode "${mode}" is not one of ${PERMISSION_MODES.join(", ")}`);
|
|
525
|
+
if (mode === "bypassPermissions" && settings.disableBypassPermissionsMode) {
|
|
526
|
+
return setupFail("bypass_disabled", "bypassPermissions mode is disabled by settings (permissions.disableBypassPermissionsMode)");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ── MCP configuration (parsed before a session is created) ─────────────────
|
|
530
|
+
const cliServers = [];
|
|
531
|
+
const mcpErrors = [];
|
|
532
|
+
for (const value of o.mcpConfig) {
|
|
533
|
+
const r = readMcpConfigArg(value, { cwd, readFile: fs.readFile, env });
|
|
534
|
+
cliServers.push(...r.servers);
|
|
535
|
+
mcpErrors.push(...r.errors);
|
|
536
|
+
}
|
|
537
|
+
if (mcpErrors.length > 0) return setupFail("invalid_mcp_config", mcpErrors.join("; "));
|
|
538
|
+
let mcpLayers = { user: [], project: [], local: [] };
|
|
539
|
+
/** Project servers left unstarted, reported in the result. */
|
|
540
|
+
const notStarted = [];
|
|
541
|
+
if (!(o.strictMcpConfig || o.bare)) {
|
|
542
|
+
const loaded = loadMcpLayers({ homedir, cwd, readFile: fs.readFile, exists: fs.exists, env });
|
|
543
|
+
for (const e of loaded.errors) warn(e);
|
|
544
|
+
// A repository's .mcp.json runs only what the user (or the organisation) approved.
|
|
545
|
+
const approved = filterProjectServers(loaded.layers.project, [loaded.approval, settings.mcpApproval]);
|
|
546
|
+
mcpLayers = { ...loaded.layers, project: approved.servers };
|
|
547
|
+
for (const s of approved.unapproved) {
|
|
548
|
+
warn(`MCP server "${s.name}" from ${s.source} was not started: project servers need approval (enabledMcpjsonServers or enableAllProjectMcpServers in ~/.claude/settings.json), or pass it with --mcp-config`);
|
|
549
|
+
notStarted.push({ name: s.name, source: s.source, transport: s.transport, status: "not_approved", tools: 0 });
|
|
550
|
+
}
|
|
551
|
+
for (const s of approved.disabled) notStarted.push({ name: s.name, source: s.source, transport: s.transport, status: "disabled", tools: 0 });
|
|
552
|
+
}
|
|
553
|
+
const servers = resolveMcpServers({ cli: cliServers, strict: o.strictMcpConfig || o.bare, ...mcpLayers });
|
|
554
|
+
const startedServers = new Set(servers.map((s) => s.name));
|
|
555
|
+
|
|
556
|
+
// ── session ────────────────────────────────────────────────────────────────
|
|
557
|
+
// CF-21: settings `model` is the default tier, below --model and COHORT_LLM_MODEL.
|
|
558
|
+
let settingsModel = settings.model;
|
|
559
|
+
if (settingsModel && !settingsModel.startsWith("cohort-")) {
|
|
560
|
+
warn(`settings model "${settingsModel}" is not a Cohort tier (cohort-…) and is ignored`);
|
|
561
|
+
settingsModel = null;
|
|
562
|
+
}
|
|
563
|
+
const model = o.model ?? env.COHORT_LLM_MODEL ?? settingsModel ?? DEFAULT_MODEL;
|
|
564
|
+
// CF-21: settings `env` reaches tool, hook and MCP child processes (each behind
|
|
565
|
+
// its own credential scrub) — never the engine's own gateway configuration.
|
|
566
|
+
/** @type {Record<string,string|undefined>} */
|
|
567
|
+
const childEnv = { ...env };
|
|
568
|
+
for (const [k, v] of Object.entries(settings.env)) {
|
|
569
|
+
if (isCoreCredentialEnvName(k)) warn(`settings env ${k} has a credential's name and is not applied`);
|
|
570
|
+
else childEnv[k] = v;
|
|
571
|
+
}
|
|
572
|
+
// CF-22: extended-shape secrets (GITHUB_TOKEN, *_SECRET, *_PASSWORD, …) a seat lets through to
|
|
573
|
+
// Bash, hooks and MCP children — from managed, --settings or user settings and the engine's own
|
|
574
|
+
// env, never from a repository's settings. Model, gateway and org credentials are never passed.
|
|
575
|
+
/** @type {Set<string>} */
|
|
576
|
+
const passthroughNames = new Set(parsePassthroughEnv(env.COHORT_ENGINE_ENV_PASSTHROUGH));
|
|
577
|
+
for (const layer of settingsLayers) {
|
|
578
|
+
const list = layer.settings?.cohort?.envPassthrough;
|
|
579
|
+
if (list === undefined) continue;
|
|
580
|
+
if (PROJECT_SCOPES.has(layer.scope)) {
|
|
581
|
+
warn(`${layer.scope} ${layer.path}: cohort.envPassthrough is honoured only in user, managed or --settings settings (ignored)`);
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
const parsed = parseEnvNameList(list);
|
|
585
|
+
if (!parsed.ok) warn(`${layer.scope} ${layer.path}: cohort.envPassthrough ${parsed.error} (ignored)`);
|
|
586
|
+
else for (const n of parsed.names) passthroughNames.add(n);
|
|
587
|
+
}
|
|
588
|
+
for (const n of passthroughNames) if (isCoreCredentialEnvName(n)) warn(`cohort.envPassthrough names ${n}, a model, gateway or org credential, which is never passed through`);
|
|
589
|
+
const envPassthrough = [...passthroughNames].filter((n) => !isCoreCredentialEnvName(n));
|
|
590
|
+
// CF-118: variables withheld for their VALUE alone (a URL with a password, a vendor token, a long
|
|
591
|
+
// random run) — reported once per run, by name and rule, never by value.
|
|
592
|
+
const byValue = withheldByValue(childEnv, { passthrough: envPassthrough });
|
|
593
|
+
if (byValue.length > 0) {
|
|
594
|
+
warn(`environment ${byValue.map((w) => `${w.name} (${w.kind})`).join(", ")} withheld from Bash, Monitor, hooks and MCP servers: the value looks like a credential (name it in user or managed cohort.envPassthrough, or a hook's or server's inheritEnv, to pass it)`);
|
|
595
|
+
}
|
|
596
|
+
const sessionsDir = env.COHORT_ENGINE_SESSIONS_DIR || path.join(deps.homedir ?? os.homedir(), ".cohort", "engine", "sessions");
|
|
597
|
+
const sessionId = o.resume ?? o.sessionId ?? (deps.newId ?? randomUUID)();
|
|
598
|
+
// One writer per session, for the whole run (conformance row 4). A caller-chosen --session-id
|
|
599
|
+
// that already exists is continued — maestro's dispatcher crash recovery and responder
|
|
600
|
+
// continuation re-spawn `--session-id <same id> <prompt>` — unless a live run holds it.
|
|
601
|
+
const lock = acquireSessionLock({ dir: sessionsDir, sessionId, ...(deps.pid != null ? { pid: deps.pid } : {}), ...(deps.isProcessAlive ? { isAlive: deps.isProcessAlive } : {}), ...(deps.processStartTime ? { processStartTime: deps.processStartTime } : {}), now });
|
|
602
|
+
/**
|
|
603
|
+
* W4-E integration: one process-identity probe for everything that records "which process holds
|
|
604
|
+
* this" — the session lock above, subagent task states and workflow journals. Injected (tests) or
|
|
605
|
+
* the defaults, which are all process-identity.mjs.
|
|
606
|
+
*/
|
|
607
|
+
const processIdentityDeps = {
|
|
608
|
+
...(deps.pid != null ? { pid: deps.pid } : {}),
|
|
609
|
+
...(deps.isProcessAlive ? { isProcessAlive: deps.isProcessAlive } : {}),
|
|
610
|
+
...(deps.processStartTime ? { processStartToken: deps.processStartTime } : {}),
|
|
611
|
+
};
|
|
612
|
+
if (!lock.ok) return setupFail(lock.error.code, lock.error.message, sessionId);
|
|
613
|
+
inputs.push({ close: lock.release });
|
|
614
|
+
const opened = openSession({
|
|
615
|
+
dir: sessionsDir,
|
|
616
|
+
sessionId,
|
|
617
|
+
mode: o.resume ? "resume" : o.sessionId ? "continue" : "new",
|
|
618
|
+
meta: { cwd, model, wire },
|
|
619
|
+
now,
|
|
620
|
+
});
|
|
621
|
+
if (!opened.ok) return setupFail(opened.error.code, opened.error.message, sessionId);
|
|
622
|
+
const session = opened.session;
|
|
623
|
+
// A continued --session-id is a resume in every way a run can see (todos, SessionStart source).
|
|
624
|
+
const resumed = Boolean(o.resume) || opened.session.continued === true;
|
|
625
|
+
|
|
626
|
+
// ── instructions and skills ────────────────────────────────────────────────
|
|
627
|
+
// --bare loads no instruction files, and only plugin skills and agents from explicit plugin dirs.
|
|
628
|
+
const instructions = o.bare ? null : loadInstructions({
|
|
629
|
+
cwd,
|
|
630
|
+
homedir,
|
|
631
|
+
userDir,
|
|
632
|
+
managedPath: deps.managedInstructionsPath !== undefined ? deps.managedInstructionsPath : defaultManagedInstructionsPath(platform),
|
|
633
|
+
readFile: fs.readFile,
|
|
634
|
+
isFile: fs.isFile,
|
|
635
|
+
realpath: fs.realpath,
|
|
636
|
+
readdir: fs.readdir,
|
|
637
|
+
isDir: fs.isDir,
|
|
638
|
+
});
|
|
639
|
+
for (const e of instructions?.errors ?? []) warn(`instructions: ${e}`);
|
|
640
|
+
// Subdirectory CLAUDE.md and path-scoped rules on first touch: one tracker per conversation (the run, each subagent).
|
|
641
|
+
const newLazyInstructions = () =>
|
|
642
|
+
instructions ? createLazyInstructions({ cwd, homedir, readFile: fs.readFile, isFile: fs.isFile, realpath: fs.realpath, loaded: instructions.files.map((f) => f.path), conditionalRules: instructions.conditionalRules }) : null;
|
|
643
|
+
const pluginDirs = [...settings.pluginDirs, ...o.pluginDirs.map((/** @type string */ d) => ({ dir: path.resolve(cwd, d), source: "--plugin-dir" }))];
|
|
644
|
+
const discovered = discoverSkills({ userDir: o.bare ? null : userDir, cwd, pluginDirs, fs });
|
|
645
|
+
for (const e of discovered.errors) warn(`skills: ${e}`);
|
|
646
|
+
const skills = wantSkill ? discovered.skills.filter((s) => !o.bare || s.source === "plugin") : [];
|
|
647
|
+
// W4-E2: `/name args` in a prompt — project and user command files, skills, plugin commands.
|
|
648
|
+
const slashSkills = discovered.skills.filter((s) => !o.bare || s.source === "plugin");
|
|
649
|
+
const discoveredCommands = discoverCommands({ userDir: o.bare ? null : userDir, cwd: o.bare ? null : cwd, pluginDirs, fs });
|
|
650
|
+
for (const e of discoveredCommands.errors) warn(`commands: ${e}`);
|
|
651
|
+
const agentDefs = discoverAgents({ cliAgents: o.agents, userDir, cwd, seatRoot: env.AGENT_ROOT || null, pluginDirs, bare: o.bare, fs });
|
|
652
|
+
for (const e of agentDefs.errors) warn(`agents: ${e}`);
|
|
653
|
+
|
|
654
|
+
// ── MCP servers: from here on they must be shut down ───────────────────────
|
|
655
|
+
const mcp = await startMcpServers(servers, { env: childEnv, envPassthrough, cwd, fetchImpl: deps.mcpFetchImpl, clientInfo: { name: "cohort-engine", version: packageVersion() } });
|
|
656
|
+
for (const e of mcp.errors) warn(e);
|
|
657
|
+
/** @type {ReturnType<typeof createSessionToolkit>|null} */
|
|
658
|
+
let kit = null;
|
|
659
|
+
/** @type {ReturnType<typeof createAgentRuntime>|null} */
|
|
660
|
+
let agentRuntime = null;
|
|
661
|
+
/** @type {ReturnType<typeof createWorkflowRuntime>|null} */
|
|
662
|
+
let workflows = null;
|
|
663
|
+
try {
|
|
664
|
+
// --output-format stream-json: every event goes through this writer.
|
|
665
|
+
const writer = o.outputFormat === "stream-json" ? createStreamJsonWriter({ write: (s) => stdout.write(s), sessionId: session.id, includePartialMessages: o.includePartialMessages, wire }) : null;
|
|
666
|
+
const mcpServerStatuses = () => [...mcp.statuses.map(({ error, ...s }) => (error ? { ...s, error } : s)), ...notStarted.filter((s) => !startedServers.has(s.name))];
|
|
667
|
+
const hidden = (/** @type {{name:string}} */ t) => isToolFullyDenied(t.name, settings.rules);
|
|
668
|
+
const context = resolveContextConfig({ tier: model, settings: settings.engine, env });
|
|
669
|
+
for (const w of context.warnings) warn(w);
|
|
670
|
+
|
|
671
|
+
const gates = createDefaultGates({ env, now, sessionId: session.id, cwd });
|
|
672
|
+
// CF-20: with the send gate in process, the seat's pre-send-audit.sh hook is
|
|
673
|
+
// skipped for the gated tools, so an allowed send is counted once.
|
|
674
|
+
const hooks = createHookRunner({ hooks: settings.hooks, sessionId: session.id, transcriptPath: session.path, cwd, permissionMode: mode, env: childEnv, envPassthrough, log: (l) => warn(`hook: ${l}`), sendGateInProcess: gates.list().includes(SEND_GATE_NAME) });
|
|
675
|
+
const additionalDirectories = [...settings.additionalDirectories, ...o.addDirs.map((/** @type string */ d) => path.resolve(cwd, d))];
|
|
676
|
+
const configDirs = env.CLAUDE_CONFIG_DIR ? [path.resolve(cwd, env.CLAUDE_CONFIG_DIR)] : [];
|
|
677
|
+
// CF-19: every permission decision follows symlinks to where a path really leads.
|
|
678
|
+
const resolvePath = deps.resolvePath ?? createPathResolver();
|
|
679
|
+
// One guard per run and per subagent, all from the same rules, gates and hooks.
|
|
680
|
+
// W4-A1: a subagent's asks carry its id, so a session approval ("always") is remembered per agent.
|
|
681
|
+
// W4-A2: a workflow child may run in its own directory (a git worktree).
|
|
682
|
+
const hostAsk = deps.host?.askPermission ?? null;
|
|
683
|
+
const guardFor = (/** @type string */ m, /** @type {string|null} */ agentId = null, /** @type {string|undefined} */ childCwd = undefined) =>
|
|
684
|
+
createToolGuard({ mode: m, rules: settings.rules, cwd: childCwd ?? cwd, homedir, additionalDirectories, configDirs, resolvePath, gates, hooks, ask: hostAsk && agentId ? (q) => hostAsk({ ...q, agentId }) : hostAsk });
|
|
685
|
+
const guard = guardFor(mode);
|
|
686
|
+
|
|
687
|
+
// ── model callers ─────────────────────────────────────────────────────────
|
|
688
|
+
const flag = (/** @type {string|undefined} */ v) => (/^(1|true|on|yes)$/i.test(String(v ?? "")) ? true : /^(0|false|off|no)$/i.test(String(v ?? "")) ? false : null);
|
|
689
|
+
const imageInput = flag(env.COHORT_ENGINE_IMAGE_INPUT) ?? settings.engine.imageInput ?? wire === "anthropic";
|
|
690
|
+
const gatewayHeaders = { "x-cohort-surface": "engine", "x-cohort-session-id": session.id };
|
|
691
|
+
// W4-E1 (row 24): one spend meter for the run, its subagents and its workflow children.
|
|
692
|
+
const budget = o.maxBudgetMicros !== undefined ? createBudgetMeter({ maxMicros: o.maxBudgetMicros }) : null;
|
|
693
|
+
// CF-156: a stalled stream ends on its own silence budget and says why on
|
|
694
|
+
// stderr. A stall or a mid-stream fault always reports — that is the
|
|
695
|
+
// evidence W13 did not have — while a merely slow stream reports only
|
|
696
|
+
// under --verbose, so an ordinary run stays quiet.
|
|
697
|
+
const idleTimeoutMs = parseIdleTimeoutMs(env.COHORT_LLM_STREAM_IDLE_TIMEOUT_MS);
|
|
698
|
+
// A budget that did not parse, or that had to be clamped into its band, is
|
|
699
|
+
// said out loud: both failures leave a guard that LOOKS armed.
|
|
700
|
+
const idleNote = idleTimeoutNote(env.COHORT_LLM_STREAM_IDLE_TIMEOUT_MS, idleTimeoutMs);
|
|
701
|
+
if (idleNote) warn(idleNote);
|
|
702
|
+
const onStreamDiagnostic = (/** @type {import('./wire/stall.mjs').StreamDiagnostic} */ d) => {
|
|
703
|
+
if (d.outcome !== "slow" || o.verbose) warn(formatStreamDiagnostic(d));
|
|
704
|
+
};
|
|
705
|
+
const modelBase = { wire, baseUrl, token, maxTokens: o.maxTokens ?? DEFAULT_MAX_OUTPUT_TOKENS, fetchImpl: deps.fetchImpl, now, sleep: deps.sleep, images: imageInput, idleTimeoutMs, onStreamDiagnostic };
|
|
706
|
+
// OpenAI wire: one prompt_cache_key state per project and tier, shared by every caller on that tier
|
|
707
|
+
// (the run, its subagents, the compaction summary), so the gateway's advertisement is seen once.
|
|
708
|
+
/** @type {Map<string, ReturnType<typeof createPromptCacheKeyState>>} */
|
|
709
|
+
const promptCaches = new Map();
|
|
710
|
+
const promptCacheFor = (/** @type string */ tier) => {
|
|
711
|
+
if (wire !== "openai") return null;
|
|
712
|
+
const key = promptCacheKey({ cwd, model: tier });
|
|
713
|
+
if (!promptCaches.has(key)) promptCaches.set(key, createPromptCacheKeyState({ key, mode: promptCacheKeyMode(env.COHORT_LLM_PROMPT_CACHE_KEY, settings.engine.promptCacheKey) }));
|
|
714
|
+
return promptCaches.get(key) ?? null;
|
|
715
|
+
};
|
|
716
|
+
// A model caller for a tier: --effort where the tier takes it, prompt caching, partial
|
|
717
|
+
// messages (`partial`) and quota events for stream-json.
|
|
718
|
+
const effortNoted = new Set();
|
|
719
|
+
const createCaller = (/** @type {{model:string, headers:Record<string,string>, parentToolUseId?:string|null, partial?:boolean, effort?:string|null}} */ { model: tier, headers, parentToolUseId = null, partial = true, effort: effortOverride = null }) => {
|
|
720
|
+
const effort = resolveEffort({ effort: effortOverride ?? o.effort ?? null, wire, tier });
|
|
721
|
+
if (effort.note && o.verbose && !effortNoted.has(tier)) {
|
|
722
|
+
effortNoted.add(tier);
|
|
723
|
+
warn(effort.note);
|
|
724
|
+
}
|
|
725
|
+
const caller = createModelCaller({ ...modelBase, model: tier, headers, reasoningEffort: effort.param, promptCache: promptCacheFor(tier), onFrame: partial ? writer?.partialFrames(parentToolUseId) : undefined });
|
|
726
|
+
return writer ? observeQuota(caller, (q) => writer.quota(q, parentToolUseId)) : caller;
|
|
727
|
+
};
|
|
728
|
+
// The tool context of a conversation (the run, or one subagent): the web tools'
|
|
729
|
+
// gateway and side-tier callers carry that conversation's attribution headers.
|
|
730
|
+
const toolContextFor = (/** @type {Record<string,string>} */ headers, /** @type {string|undefined} */ childCwd) => {
|
|
731
|
+
/** @type {Map<string, ReturnType<typeof createModelCaller>>} */
|
|
732
|
+
const tierCallers = new Map();
|
|
733
|
+
const models = {
|
|
734
|
+
/** A request on another tier through the same gateway (WebFetch uses cohort-fast). @param {{tier:string, system:string, messages:any[], maxTokens?:number, signal?:AbortSignal}} r */
|
|
735
|
+
call({ tier, system: sys, messages, maxTokens, signal }) {
|
|
736
|
+
// --max-budget-usd spent: no further model call, a tool's side call included (never billed).
|
|
737
|
+
if (budget?.exhausted()) return Promise.resolve({ ok: false, accepted: false, error: { kind: "budget", code: "max_budget_usd", status: 0, message: "the run's --max-budget-usd is spent" }, cohort: null, apiMs: 0 });
|
|
738
|
+
if (!tierCallers.has(tier)) tierCallers.set(tier, createModelCaller({ ...modelBase, headers, model: tier }));
|
|
739
|
+
return /** @type {ReturnType<typeof createModelCaller>} */ (tierCallers.get(tier))({ system: sys, messages, tools: [], signal, maxTokens });
|
|
740
|
+
},
|
|
741
|
+
};
|
|
742
|
+
return { ...createToolContext({ cwd: childCwd ?? cwd, env: childEnv, useRipgrep: deps.useRipgrep, envPassthrough }), gateway: { baseUrl, token, headers, fetchImpl: deps.fetchImpl }, models, web: deps.web };
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
// ── session tools: the task list, background shells, subagents ───────────
|
|
746
|
+
const todoState = createTodoState({
|
|
747
|
+
initial: resumed ? loadSessionTodos(session.path) : [],
|
|
748
|
+
onChange: (todos) => {
|
|
749
|
+
if (!appendJsonl(session.path, { type: "todos", sessionId: session.id, ts: new Date(now()).toISOString(), todos })) session.writeFailures++;
|
|
750
|
+
writer?.todos(todos);
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
// Events for the model between requests (CF-50): background shells that ended, and — in a
|
|
754
|
+
// session — monitor lines; background subagents' reports join as a source below.
|
|
755
|
+
const notices = deps.host?.notices ?? createNotificationQueue();
|
|
756
|
+
kit = createSessionToolkit({ cwd, env: childEnv, envPassthrough, spillDir: path.join(sessionsDir, "shells", session.id), todoState, onShellExit: (s) => notices.push(shellExitNotice(s)) });
|
|
757
|
+
const promptBase = { cwd, platform: `${os.platform()} ${os.release()}`, date: new Date(now()).toISOString().slice(0, 10), instructions: instructions ? formatInstructions(instructions) : null };
|
|
758
|
+
// Child refusals belong to the turn that started the child. One that arrives
|
|
759
|
+
// after that turn's result was written is emitted on its own (stream-json).
|
|
760
|
+
let turnNo = 0;
|
|
761
|
+
/** @type {Map<number, any[]>} */
|
|
762
|
+
const childDenials = new Map();
|
|
763
|
+
const reportedTurns = new Set();
|
|
764
|
+
const addChildDenials = (/** @type {any[]} */ ds, /** @type {{turn:number|null}} */ meta) => {
|
|
765
|
+
const t = meta?.turn ?? turnNo;
|
|
766
|
+
if (reportedTurns.has(t)) {
|
|
767
|
+
writer?.emit({ type: "system", subtype: "cohort_late_permission_denials", turn: t, permission_denials: ds, session_id: session.id });
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
childDenials.set(t, [...(childDenials.get(t) ?? []), ...ds]);
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* A subagent's own context (W3 integration): the parent's ToolSearch is bound to
|
|
775
|
+
* the parent's deferred state, so a child whose MCP tools cross the deferral
|
|
776
|
+
* threshold gets its own; and a context manager on the child's tier, so a long
|
|
777
|
+
* child compacts (its summary on the child's caller, billed to the child).
|
|
778
|
+
* @param {{def:any, model:string, tools:any[], headers:Record<string,string>, parentToolUseId:string|null, onCompact:(m:any)=>void}} c
|
|
779
|
+
*/
|
|
780
|
+
const prepareChild = ({ def, model: tier, tools: granted, headers, parentToolUseId, onCompact }) => {
|
|
781
|
+
const childContext = resolveContextConfig({ tier, settings: settings.engine, env }).config;
|
|
782
|
+
let childTools = granted.filter((t) => t.name !== "ToolSearch");
|
|
783
|
+
/** @type {ReturnType<typeof createDeferredTools>|null} */
|
|
784
|
+
let childDeferred = null;
|
|
785
|
+
if (!hidden({ name: "ToolSearch" }) && shouldDeferTools({ tools: childTools, contextWindow: childContext.contextWindow })) {
|
|
786
|
+
childDeferred = createDeferredTools({ tools: [] });
|
|
787
|
+
childTools = stableToolOrder([...childTools.filter((t) => !t.mcp), createToolSearchTool(childDeferred), ...childTools.filter((t) => t.mcp)]);
|
|
788
|
+
childDeferred.replace(childTools);
|
|
789
|
+
}
|
|
790
|
+
const childManager = createContextManager({
|
|
791
|
+
config: childContext,
|
|
792
|
+
cwd,
|
|
793
|
+
summarise: createCaller({ model: tier, headers, parentToolUseId, partial: false }),
|
|
794
|
+
tools: childTools,
|
|
795
|
+
deferred: childDeferred,
|
|
796
|
+
hooks,
|
|
797
|
+
lazy: newLazyInstructions(),
|
|
798
|
+
// Children use the MCP tool list as it was when they started; list changes are the run's to pick up.
|
|
799
|
+
mcp: null,
|
|
800
|
+
input: null,
|
|
801
|
+
onCompact: ({ message }) => onCompact(message),
|
|
802
|
+
log: (l) => warn(`subagent ${def.name}: ${l}`),
|
|
803
|
+
});
|
|
804
|
+
return { tools: childTools, sent: childDeferred ? childDeferred.active() : childTools, notes: childDeferred ? formatDeferredNote(childDeferred) : null, context: childManager };
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
// cohort.maxAgentDepth from managed, --settings or user settings: a repository cannot raise it.
|
|
808
|
+
const depthSetting = settingsLayers.filter((l) => l.scope !== "project" && l.scope !== "local").map((l) => Number(l.settings?.cohort?.maxAgentDepth)).find((n) => Number.isInteger(n) && n >= 1);
|
|
809
|
+
agentRuntime = createAgentRuntime({
|
|
810
|
+
agents: agentDefs.agents,
|
|
811
|
+
sessionId: session.id,
|
|
812
|
+
sessionsDir,
|
|
813
|
+
cwd,
|
|
814
|
+
wire,
|
|
815
|
+
parentModel: model,
|
|
816
|
+
parentMode: mode,
|
|
817
|
+
parentAgentId: session.id,
|
|
818
|
+
maxDepth: depthSetting ?? DEFAULT_MAX_AGENT_DEPTH,
|
|
819
|
+
createCaller,
|
|
820
|
+
createGuard: ({ mode: m, agentId, cwd: childCwd }) => guardFor(m, agentId ?? null, childCwd),
|
|
821
|
+
buildSystem: ({ def, toolNames, notes, cwd: childCwd }) => buildSystemPrompt({ ...promptBase, ...(childCwd ? { cwd: childCwd } : {}), toolNames, append: subagentPrompt(def), skills: toolNames.includes("Skill") ? formatSkillListing(skills) : null, notes: notes ?? null }),
|
|
822
|
+
createToolContext: ({ headers, cwd: childCwd }) => toolContextFor(headers, childCwd),
|
|
823
|
+
prepareChild,
|
|
824
|
+
hooks,
|
|
825
|
+
onMessage: (m, { parentToolUseId }) => writer?.message(m, parentToolUseId),
|
|
826
|
+
onDenials: addChildDenials,
|
|
827
|
+
currentTurn: () => turnNo,
|
|
828
|
+
checkSubagent: ({ toolName, input, mode: m }) => evaluatePermission({ toolName, input, readOnly: true, mode: m, rules: settings.rules, cwd, homedir, additionalDirectories, configDirs, resolvePath }),
|
|
829
|
+
log: (l) => warn(l),
|
|
830
|
+
maxTurns: o.maxTurns,
|
|
831
|
+
wallClockMs: o.wallClockMs,
|
|
832
|
+
budget,
|
|
833
|
+
now,
|
|
834
|
+
shells: kit.shells,
|
|
835
|
+
onNotify: () => notices.poke(),
|
|
836
|
+
// W4-E2: task states record the process; a continued session reads them back (row 23).
|
|
837
|
+
// W4-E integration: the same identity probe as the session lock (process-identity.mjs).
|
|
838
|
+
...processIdentityDeps,
|
|
839
|
+
});
|
|
840
|
+
notices.addSource(agentRuntime.drainNotifications);
|
|
841
|
+
// W4-A2: workflow runs. Their agent() children are this run's subagents (same ceiling,
|
|
842
|
+
// headers, usage records), and each agent type passes the Task(<type>) rules.
|
|
843
|
+
const budgetTokens = Number(env.COHORT_ENGINE_WORKFLOW_BUDGET_TOKENS);
|
|
844
|
+
const runtimeForChildren = agentRuntime;
|
|
845
|
+
workflows = createWorkflowRuntime({
|
|
846
|
+
sessionId: session.id,
|
|
847
|
+
sessionsDir,
|
|
848
|
+
cwd,
|
|
849
|
+
agents: agentDefs.agents,
|
|
850
|
+
runChild: (p) => runtimeForChildren.runChild(p),
|
|
851
|
+
checkAgent: ({ agentType }) => evaluatePermission({ toolName: "Task", input: { subagent_type: agentType }, readOnly: true, mode, rules: settings.rules, cwd, homedir, additionalDirectories, configDirs, resolvePath }),
|
|
852
|
+
checkRead: ({ path: file }) => evaluatePermission({ toolName: "Read", input: { file_path: file }, readOnly: true, mode, rules: settings.rules, cwd, homedir, additionalDirectories, configDirs, resolvePath }),
|
|
853
|
+
now,
|
|
854
|
+
// Workflow journals judge a run's process with the same probe as task states and the session lock.
|
|
855
|
+
...processIdentityDeps,
|
|
856
|
+
budgetTokens: Number.isInteger(budgetTokens) && budgetTokens > 0 ? budgetTokens : null,
|
|
857
|
+
// W4-E2 (CF-90): an optional time limit per run, and the busy-watchdog and budget-grace knobs.
|
|
858
|
+
...workflowTimingFromEnv(env),
|
|
859
|
+
onNotify: () => notices.poke(),
|
|
860
|
+
});
|
|
861
|
+
// Workflow results reach the loop through the same event queue as background subagents' reports.
|
|
862
|
+
notices.addSource(workflows.drainNotifications);
|
|
863
|
+
// W4-E2: TaskOutput and TaskStop take workflow run ids too.
|
|
864
|
+
agentRuntime.setWorkflowRuns(workflows);
|
|
865
|
+
|
|
866
|
+
// ── the run's tools ───────────────────────────────────────────────────────
|
|
867
|
+
// Tool order is part of the cached prefix: built-ins (the session Bash in Bash's
|
|
868
|
+
// place), Skill, session tools, MCP resource tools, then MCP tools by name.
|
|
869
|
+
// The web tools leave the default set when switched off (an explicit --tools list keeps them).
|
|
870
|
+
const webOff = o.tools == null && !webToolsEnabled({ env: env.COHORT_ENGINE_WEB_TOOLS, setting: settings.engine.webTools });
|
|
871
|
+
const baseTools = [
|
|
872
|
+
...kit.withSessionBash(builtins.filter((t) => !(webOff && WEB_TOOL_NAMES.includes(t.name)))),
|
|
873
|
+
...(skills.length > 0 ? [createSkillTool(skills, fs)] : []),
|
|
874
|
+
...kit.tools({ wanted: wantedSessionTools(o.tools), agentTools: agentRuntime.tools(), workflowTools: createWorkflowTools(workflows) }),
|
|
875
|
+
...createMcpResourceTools(() => mcp.connections, { isServerDenied: (name) => isToolFullyDenied(`mcp__${name}`, settings.rules) }),
|
|
876
|
+
// W4-A1: ListAgents, SendMessage, ScheduleWakeup, Monitor. A Monitor command is also held to the Bash deny/ask rules.
|
|
877
|
+
...(deps.host
|
|
878
|
+
? deps.host
|
|
879
|
+
.tools({
|
|
880
|
+
session,
|
|
881
|
+
kit,
|
|
882
|
+
checkCommand: (command) => evaluatePermission({ toolName: "Bash", input: { command }, readOnly: false, mode: "bypassPermissions", rules: settings.rules, cwd, homedir, additionalDirectories, configDirs, resolvePath }),
|
|
883
|
+
})
|
|
884
|
+
.filter((t) => o.tools == null || o.tools.includes(t.name))
|
|
885
|
+
: []),
|
|
886
|
+
].filter((t) => !hidden(t));
|
|
887
|
+
let tools = stableToolOrder([...baseTools, ...mcp.tools.filter((t) => !hidden(t))]);
|
|
888
|
+
/** @type {ReturnType<typeof createDeferredTools>|null} */
|
|
889
|
+
let deferred = null;
|
|
890
|
+
/** @type {any} */
|
|
891
|
+
let toolSearch = null;
|
|
892
|
+
if (!hidden({ name: "ToolSearch" }) && shouldDeferTools({ tools, contextWindow: context.config.contextWindow })) {
|
|
893
|
+
deferred = createDeferredTools({ tools: [] });
|
|
894
|
+
toolSearch = createToolSearchTool(deferred);
|
|
895
|
+
}
|
|
896
|
+
const composeTools = (/** @type any[] */ mcpTools) => stableToolOrder([...baseTools, ...(toolSearch ? [toolSearch] : []), ...mcpTools.filter((t) => !hidden(t))]);
|
|
897
|
+
if (deferred) {
|
|
898
|
+
tools = composeTools(mcp.tools);
|
|
899
|
+
deferred.replace(tools);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
const system = buildSystemPrompt({
|
|
903
|
+
...promptBase,
|
|
904
|
+
toolNames: (deferred ? deferred.active() : tools).map((t) => t.name),
|
|
905
|
+
systemPrompt: o.systemPrompt ?? null,
|
|
906
|
+
append: o.appendSystemPrompt ?? null,
|
|
907
|
+
skills: formatSkillListing(skills),
|
|
908
|
+
notes: deferred ? formatDeferredNote(deferred) : null,
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
const sessionStart = await hooks.sessionStart({ source: resumed ? "resume" : "startup" });
|
|
912
|
+
writer?.init({ model, tools: tools.map((t) => t.name), mcpServers: mcpServerStatuses(), permissionMode: mode, cwd, agents: agentDefs.agents.map((a) => a.name), wire });
|
|
913
|
+
|
|
914
|
+
// stream-json input: one reader for user turns and control messages. Invalid lines
|
|
915
|
+
// are reported as they are read; user text keeps the note on blocks it left out.
|
|
916
|
+
/** @type {ReturnType<typeof createStreamInput>|null} */
|
|
917
|
+
let streamInput = null;
|
|
918
|
+
if (streamInputWanted) {
|
|
919
|
+
const source = deps.stdinStream ?? (deps.stdinLines ? linesAsChunks(deps.stdinLines) : process.stdin);
|
|
920
|
+
streamInput = createStreamInput(source, {
|
|
921
|
+
parse: parseInputItem,
|
|
922
|
+
onInvalid: (e) => {
|
|
923
|
+
warn(`stream-json input: ${e}`);
|
|
924
|
+
writer?.emit({ type: "system", subtype: "cohort_input_error", error: e, session_id: session.id });
|
|
925
|
+
},
|
|
926
|
+
});
|
|
927
|
+
inputs.push(streamInput);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const callModel = createCaller({ model, headers: gatewayHeaders });
|
|
931
|
+
// One context manager for the session: its calibration, compaction history and
|
|
932
|
+
// loaded deferred tools carry from turn to turn. A control message is drained at
|
|
933
|
+
// a turn boundary only when it arrived before the next user message.
|
|
934
|
+
const manager = createContextManager({
|
|
935
|
+
config: context.config,
|
|
936
|
+
cwd,
|
|
937
|
+
summarise: createCaller({ model, headers: gatewayHeaders, partial: false }),
|
|
938
|
+
tools,
|
|
939
|
+
deferred,
|
|
940
|
+
hooks,
|
|
941
|
+
lazy: newLazyInstructions(),
|
|
942
|
+
mcp,
|
|
943
|
+
composeTools,
|
|
944
|
+
input: streamInput ? { drain: () => /** @type {NonNullable<typeof streamInput>} */ (streamInput).drainControls() } : null,
|
|
945
|
+
onCompact: ({ message }) => appendMessage(session, transcriptMessage(message), now),
|
|
946
|
+
log: (l) => warn(l),
|
|
947
|
+
});
|
|
948
|
+
// Front-door tools belong to the session, not to its subagents.
|
|
949
|
+
agentRuntime.setParentTools(() => manager.tools.filter((/** @type any */ t) => !t.sessionOnly));
|
|
950
|
+
const toolContext = toolContextFor(gatewayHeaders);
|
|
951
|
+
// A headless run waits for background subagents' reports and workflow results together (their
|
|
952
|
+
// drains are sources of `notices`, so each notification is taken exactly once).
|
|
953
|
+
const notifications = mergeNotificationSources([agentRuntime, workflows]);
|
|
954
|
+
|
|
955
|
+
// One turn per prompt: the -p prompt, then (stream-json input) each user line until EOF.
|
|
956
|
+
let pendingPrompt = prompt ? String(prompt) : null;
|
|
957
|
+
const nextTurn = async () => {
|
|
958
|
+
if (pendingPrompt !== null) {
|
|
959
|
+
const t = pendingPrompt;
|
|
960
|
+
pendingPrompt = null;
|
|
961
|
+
return t;
|
|
962
|
+
}
|
|
963
|
+
if (!streamInput) return null;
|
|
964
|
+
const u = await streamInput.nextUser();
|
|
965
|
+
return u.ok ? u.text : null;
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* W4-E2: a prompt that invokes a command or skill carries its instructions instead (hooks saw the
|
|
970
|
+
* prompt as typed). Before expanding, the rules are consulted: `SlashCommand(/<name>)` (typed name
|
|
971
|
+
* and canonical name; `SlashCommand(/kit:*)` for a plugin; a bare `SlashCommand` for all) and, for a
|
|
972
|
+
* skill, `Skill(<name>)`. A deny or ask rule (nobody can be asked mid-prompt) refuses it: the prompt
|
|
973
|
+
* passes on as written with a note, and its frontmatter `model` does not apply. An unknown name
|
|
974
|
+
* passes with a note.
|
|
975
|
+
* @param {string} typed
|
|
976
|
+
* @returns {{text:string, model:string|null, invoked:{kind:string, name:string, file:string|null}|null}}
|
|
977
|
+
*/
|
|
978
|
+
const expandPrompt = (typed) => {
|
|
979
|
+
const resolved = resolveSlashCommand({ text: typed, commands: discoveredCommands.commands, skills: slashSkills });
|
|
980
|
+
if (resolved.kind === "none") return { text: typed, model: null, invoked: null };
|
|
981
|
+
if (resolved.kind !== "unknown") {
|
|
982
|
+
const canonical = resolved.kind === "skill" ? resolved.skill.name : resolved.command.name;
|
|
983
|
+
const judge = (/** @type string */ toolName, /** @type any */ input) => evaluatePermission({ toolName, input, readOnly: true, mode, rules: settings.rules, cwd, homedir, additionalDirectories, configDirs, resolvePath });
|
|
984
|
+
const verdicts = [...new Set([resolved.name, canonical])].map((n) => judge("SlashCommand", { command: `/${n}` }));
|
|
985
|
+
if (resolved.kind === "skill") verdicts.push(judge("Skill", { skill: canonical }));
|
|
986
|
+
const refused = verdicts.find((v) => v.behavior !== "allow");
|
|
987
|
+
if (refused) {
|
|
988
|
+
const what = resolved.kind === "skill" ? `the "${canonical}" skill` : `the /${canonical} command`;
|
|
989
|
+
warn(`/${resolved.name}: ${what.replace(/"/g, "")} is refused (${refused.reason})`);
|
|
990
|
+
return { text: `${typed}\n\n[Note: ${what} is not available in this session (${refused.reason}); the message above is passed on as written.]`, model: null, invoked: null };
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
const x = expandSlashCommand(resolved, fs, typed);
|
|
994
|
+
if (!x.ok) {
|
|
995
|
+
warn(x.error);
|
|
996
|
+
return { text: `${typed}\n\n[Note: ${x.error}]`, model: null, invoked: null };
|
|
997
|
+
}
|
|
998
|
+
if (resolved.kind === "unknown") warn(`/${resolved.name} is not a command or skill here; passed on as written`);
|
|
999
|
+
return x;
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
let history = resumeHistory(session.messages);
|
|
1003
|
+
let first = true;
|
|
1004
|
+
/** @type {any} */
|
|
1005
|
+
let last = null;
|
|
1006
|
+
for (let text = await nextTurn(); text !== null; text = await nextTurn()) {
|
|
1007
|
+
const turnStarted = first ? started : now();
|
|
1008
|
+
const submitted = await hooks.userPromptSubmit({ prompt: text });
|
|
1009
|
+
const hookStop = (first ? sessionStart.stopReason : null) ?? submitted.stopReason;
|
|
1010
|
+
if (submitted.blockReason || hookStop) {
|
|
1011
|
+
const [code, message] = submitted.blockReason
|
|
1012
|
+
? ["prompt_blocked", `the prompt was blocked by a UserPromptSubmit hook: ${submitted.blockReason}`]
|
|
1013
|
+
: ["hook_stopped", `a hook ended the run before it started: ${hookStop}`];
|
|
1014
|
+
if (!streamInput) return setupFail(code, message, session.id);
|
|
1015
|
+
last = buildSetupErrorResult({ sessionId: session.id, code, message, durationMs: now() - turnStarted, wire });
|
|
1016
|
+
writer?.result(last);
|
|
1017
|
+
if (hookStop) break;
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/** @type {Array<{type:'text', text:string}>} */
|
|
1022
|
+
const blocks = [];
|
|
1023
|
+
if (first && sessionStart.additionalContext) blocks.push({ type: "text", text: `Context from the session start hooks:\n${sessionStart.additionalContext}` });
|
|
1024
|
+
const slash = expandPrompt(text);
|
|
1025
|
+
blocks.push({ type: "text", text: slash.text });
|
|
1026
|
+
/** A command's `model` runs this turn on that model's tier. */
|
|
1027
|
+
let turnCaller = callModel;
|
|
1028
|
+
if (slash.model) {
|
|
1029
|
+
const r = resolveAgentModel(slash.model, model);
|
|
1030
|
+
if (r.note) warn(`/${slash.invoked?.name}: ${r.note}`);
|
|
1031
|
+
if (r.model !== model) turnCaller = createCaller({ model: r.model, headers: gatewayHeaders });
|
|
1032
|
+
}
|
|
1033
|
+
if (submitted.additionalContext) blocks.push({ type: "text", text: `Context from the prompt hooks:\n${submitted.additionalContext}` });
|
|
1034
|
+
const prompted = { role: "user", content: blocks };
|
|
1035
|
+
first = false;
|
|
1036
|
+
|
|
1037
|
+
const denialsBefore = guard.denials.length;
|
|
1038
|
+
const compactionsBefore = manager.stats().compactions.length;
|
|
1039
|
+
turnNo++;
|
|
1040
|
+
appendMessage(session, prompted, now);
|
|
1041
|
+
const outcome = await runLoop({
|
|
1042
|
+
callModel: turnCaller,
|
|
1043
|
+
system,
|
|
1044
|
+
messages: [...history, prompted],
|
|
1045
|
+
tools: manager.tools,
|
|
1046
|
+
toolContext,
|
|
1047
|
+
maxTurns: o.maxTurns,
|
|
1048
|
+
wallClockMs: o.wallClockMs,
|
|
1049
|
+
// A session interrupts one turn at a time; its own shutdown is deps.signal.
|
|
1050
|
+
signal: deps.host ? deps.host.turnSignal() : deps.signal,
|
|
1051
|
+
now,
|
|
1052
|
+
onMessage: (m) => {
|
|
1053
|
+
appendMessage(session, transcriptMessage(m), now);
|
|
1054
|
+
writer?.message(m);
|
|
1055
|
+
},
|
|
1056
|
+
guard,
|
|
1057
|
+
onStop: ({ stopHookActive }) => hooks.stop({ stopHookActive }),
|
|
1058
|
+
drainNotifications: notices.drain,
|
|
1059
|
+
// A session's turn does not wait for background agents or workflows: their reports arrive as events.
|
|
1060
|
+
awaitNotifications: deps.host ? undefined : notifications.awaitNotifications,
|
|
1061
|
+
context: manager,
|
|
1062
|
+
budget,
|
|
1063
|
+
});
|
|
1064
|
+
// A run that ended without waiting (interrupted, out of turns) stops its background agents and workflows; their usage still counts.
|
|
1065
|
+
// A session keeps them across turns (stopped when it ends, in the finally below).
|
|
1066
|
+
if (!deps.host) {
|
|
1067
|
+
await workflows.stopAll();
|
|
1068
|
+
await agentRuntime.stopAll();
|
|
1069
|
+
}
|
|
1070
|
+
// The loop's history, compacted where compaction ran: the next turn continues from it.
|
|
1071
|
+
history = outcome.messages;
|
|
1072
|
+
|
|
1073
|
+
const children = agentRuntime.drainRecords();
|
|
1074
|
+
const total = aggregateUsage({ tier: outcome.modelTier || model, usage: outcome.usage, costMicros: outcome.costMicros, requestIds: outcome.requestIds }, children);
|
|
1075
|
+
const result = buildResult({
|
|
1076
|
+
outcome: children.length > 0 ? { ...outcome, usage: total.usage, costMicros: total.costMicros, requestIds: total.requestIds } : outcome,
|
|
1077
|
+
sessionId: session.id,
|
|
1078
|
+
durationMs: now() - turnStarted,
|
|
1079
|
+
wire,
|
|
1080
|
+
session: { path: session.path, writeFailures: session.writeFailures, skipped: session.skipped },
|
|
1081
|
+
});
|
|
1082
|
+
result.permission_denials = [...guard.denials.slice(denialsBefore), ...(childDenials.get(turnNo) ?? [])];
|
|
1083
|
+
childDenials.delete(turnNo);
|
|
1084
|
+
reportedTurns.add(turnNo);
|
|
1085
|
+
result.modelUsage = total.modelUsage;
|
|
1086
|
+
result.cohort.permissionMode = mode;
|
|
1087
|
+
result.cohort.mcpServers = mcpServerStatuses();
|
|
1088
|
+
result.cohort.modelUsage = total.cohortModelUsage;
|
|
1089
|
+
result.cohort.todos = todoState.todos;
|
|
1090
|
+
result.cohort.agents = children.map((c) => ({ agentId: c.agentId, agentType: c.agentType, tier: c.tier, costMicros: c.costMicros, background: c.background, numTurns: c.numTurns, stop: c.stop }));
|
|
1091
|
+
// Context stats for the session so far; `compactions` are this turn's.
|
|
1092
|
+
const stats = manager.stats();
|
|
1093
|
+
result.cohort.context = { ...stats, compactions: stats.compactions.slice(compactionsBefore) };
|
|
1094
|
+
// --max-budget-usd: the cap and what the run has spent so far (subagents and workflows included).
|
|
1095
|
+
if (budget) {
|
|
1096
|
+
result.cohort.budget = budget.snapshot();
|
|
1097
|
+
// CF-157: say it ONCE per run when the gateway priced nothing, so a cap
|
|
1098
|
+
// that CANNOT bind is never read as a cap that merely was not reached.
|
|
1099
|
+
const budgetNotice = budget.notice();
|
|
1100
|
+
if (budgetNotice) warn(budgetNotice);
|
|
1101
|
+
}
|
|
1102
|
+
if (slash.invoked) result.cohort.slashCommand = slash.invoked;
|
|
1103
|
+
appendResult(session, result, now);
|
|
1104
|
+
last = result;
|
|
1105
|
+
|
|
1106
|
+
if (writer) writer.result(result);
|
|
1107
|
+
else if (o.outputFormat === "json") stdout.write(JSON.stringify(result) + "\n");
|
|
1108
|
+
else if (result.is_error) stderr.write(`cohort run: ${result.result}\n`);
|
|
1109
|
+
else stdout.write(`${result.result}\n`);
|
|
1110
|
+
if (deps.signal?.aborted) break;
|
|
1111
|
+
}
|
|
1112
|
+
if (streamInput && streamInput.pendingControls > 0) warn(`stream-json input: ${streamInput.pendingControls} control message(s) arrived after the last user message and were not applied`);
|
|
1113
|
+
if (last === null) {
|
|
1114
|
+
// stream-json input that ended before any user message: still one result.
|
|
1115
|
+
last = buildSetupErrorResult({ sessionId: session.id, code: "no_input", message: "the stream-json input ended before any user message", durationMs: now() - started, wire });
|
|
1116
|
+
writer?.result(last);
|
|
1117
|
+
}
|
|
1118
|
+
return last?.is_error ? 1 : 0;
|
|
1119
|
+
} finally {
|
|
1120
|
+
await workflows?.stopAll();
|
|
1121
|
+
await agentRuntime?.stopAll();
|
|
1122
|
+
await kit?.dispose();
|
|
1123
|
+
await mcp.close();
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* One stream-json input line: control messages as context/stream-input.mjs reads
|
|
1129
|
+
* them; a user message's text as output/stream-json.mjs reads it, which notes
|
|
1130
|
+
* the content blocks it left out. Pure.
|
|
1131
|
+
* @param {string} line
|
|
1132
|
+
*/
|
|
1133
|
+
function parseInputItem(line) {
|
|
1134
|
+
const item = parseStreamInputLine(line);
|
|
1135
|
+
if (item.kind !== "user") return item;
|
|
1136
|
+
const turn = parseInputLine(line);
|
|
1137
|
+
return turn && turn.ok ? { ...item, text: turn.text } : item;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/** Lines (tests' `stdinLines`) as newline-terminated chunks for the stream reader. @param {AsyncIterable<string>|Iterable<string>} lines */
|
|
1141
|
+
async function* linesAsChunks(lines) {
|
|
1142
|
+
for await (const line of lines) yield `${line}\n`;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/** The package version, for the MCP clientInfo. */
|
|
1146
|
+
function packageVersion() {
|
|
1147
|
+
try {
|
|
1148
|
+
return JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version || "0";
|
|
1149
|
+
} catch {
|
|
1150
|
+
return "0";
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/** Read all of stdin when it is not a terminal. */
|
|
1155
|
+
export async function readProcessStdin() {
|
|
1156
|
+
if (process.stdin.isTTY) return "";
|
|
1157
|
+
const chunks = [];
|
|
1158
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
1159
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* The process edge: wires SIGINT/SIGTERM to an abort so an interrupted run
|
|
1164
|
+
* still closes its transcript and prints a result.
|
|
1165
|
+
* @param {string[]} argv
|
|
1166
|
+
*/
|
|
1167
|
+
export async function runFromProcess(argv) {
|
|
1168
|
+
// W4-A1: `cli.mjs session …` is the long-lived front door (session-runtime/runner.mjs).
|
|
1169
|
+
if (argv[0] === "session") return (await import("./session-runtime/runner.mjs")).runSessionFromProcess(argv.slice(1));
|
|
1170
|
+
const controller = new AbortController();
|
|
1171
|
+
const onSignal = () => controller.abort();
|
|
1172
|
+
process.once("SIGINT", onSignal);
|
|
1173
|
+
process.once("SIGTERM", onSignal);
|
|
1174
|
+
try {
|
|
1175
|
+
return await runCli(argv, { signal: controller.signal, readStdin: readProcessStdin });
|
|
1176
|
+
} finally {
|
|
1177
|
+
process.removeListener("SIGINT", onSignal);
|
|
1178
|
+
process.removeListener("SIGTERM", onSignal);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/** Same realpath-resolved comparison bin/maestro.mjs uses (npm bins are symlinks). */
|
|
1183
|
+
function isEntrypoint() {
|
|
1184
|
+
if (!process.argv[1]) return false;
|
|
1185
|
+
try {
|
|
1186
|
+
return import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
|
|
1187
|
+
} catch {
|
|
1188
|
+
return false; // argv[1] is not a real path (e.g. `node -e`): not the entrypoint
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
if (isEntrypoint()) {
|
|
1193
|
+
// Not a top-level await: `session` imports modules that import this one, and a module
|
|
1194
|
+
// still evaluating (held open by its own top-level await) would never finish loading.
|
|
1195
|
+
runFromProcess(process.argv.slice(2)).then(
|
|
1196
|
+
(code) => {
|
|
1197
|
+
process.exitCode = code;
|
|
1198
|
+
},
|
|
1199
|
+
(e) => {
|
|
1200
|
+
process.stderr.write(`${e instanceof Error ? (e.stack ?? e.message) : String(e)}\n`);
|
|
1201
|
+
process.exitCode = 1;
|
|
1202
|
+
},
|
|
1203
|
+
);
|
|
1204
|
+
}
|