@cohortapp/agent-sdk 2.17.0 → 2.18.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +18 -0
- package/.env.example +18 -5
- package/README.md +1 -0
- package/bin/maestro.mjs +62 -0
- package/docs/guides/billing-console-keys.md +60 -0
- package/docs/guides/front-door-session.md +54 -9
- package/docs/guides/mac-mini.md +20 -25
- package/docs/guides/setup-wizard.md +1 -1
- package/docs/runbooks/fleet-rollout.md +156 -0
- package/docs/runbooks/mac-mini-bootstrap.md +12 -14
- package/lib/action-executor.js +19 -3
- package/lib/budget-guard.mjs +279 -3
- package/lib/channels/base-adapter.mjs +3 -1
- package/lib/channels/contract.mjs +2 -1
- package/lib/channels/inbox-item.mjs +8 -0
- package/lib/claude-bin.mjs +5 -6
- package/lib/cli/doctor-checks.mjs +141 -10
- package/lib/cli/global-setup-extras.mjs +5 -1
- package/lib/cli/inbox.mjs +100 -15
- package/lib/cli/seat-auth.mjs +463 -0
- package/lib/cli/session.mjs +80 -12
- package/lib/collective/capture.mjs +8 -6
- package/lib/collective/global-config.mjs +63 -1
- package/lib/collective/presence.mjs +142 -5
- package/lib/comms/send-gate.mjs +559 -1
- package/lib/diagnostics/alerts.mjs +49 -0
- package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
- package/lib/engine/agents/definitions.mjs +343 -0
- package/lib/engine/agents/persist.mjs +275 -0
- package/lib/engine/agents/runtime.mjs +748 -0
- package/lib/engine/agents/usage.mjs +95 -0
- package/lib/engine/auth-status.mjs +139 -0
- package/lib/engine/budget.mjs +194 -0
- package/lib/engine/cli.mjs +1204 -0
- package/lib/engine/commands/index.mjs +269 -0
- package/lib/engine/context/budget.mjs +219 -0
- package/lib/engine/context/cache.mjs +125 -0
- package/lib/engine/context/child-env.mjs +215 -0
- package/lib/engine/context/compaction.mjs +342 -0
- package/lib/engine/context/images.mjs +90 -0
- package/lib/engine/context/instructions.mjs +327 -0
- package/lib/engine/context/lazy-instructions.mjs +169 -0
- package/lib/engine/context/manager.mjs +182 -0
- package/lib/engine/context/real-path.mjs +91 -0
- package/lib/engine/context/secret-values.mjs +163 -0
- package/lib/engine/context/settings.mjs +274 -0
- package/lib/engine/context/stream-input.mjs +159 -0
- package/lib/engine/guard.mjs +152 -0
- package/lib/engine/hooks.mjs +713 -0
- package/lib/engine/loop.mjs +560 -0
- package/lib/engine/mcp/client.mjs +254 -0
- package/lib/engine/mcp/config.mjs +301 -0
- package/lib/engine/mcp/http.mjs +201 -0
- package/lib/engine/mcp/index.mjs +146 -0
- package/lib/engine/mcp/jsonrpc.mjs +147 -0
- package/lib/engine/mcp/naming.mjs +66 -0
- package/lib/engine/mcp/resources.mjs +89 -0
- package/lib/engine/mcp/results.mjs +133 -0
- package/lib/engine/mcp/stdio.mjs +137 -0
- package/lib/engine/mcp/supervisor.mjs +116 -0
- package/lib/engine/messages.mjs +104 -0
- package/lib/engine/output/json.mjs +164 -0
- package/lib/engine/output/stream-json.mjs +266 -0
- package/lib/engine/permissions.mjs +845 -0
- package/lib/engine/process-identity.mjs +164 -0
- package/lib/engine/process-tree.mjs +551 -0
- package/lib/engine/prompt.mjs +60 -0
- package/lib/engine/session/store.mjs +299 -0
- package/lib/engine/session-runtime/args.mjs +97 -0
- package/lib/engine/session-runtime/host.mjs +143 -0
- package/lib/engine/session-runtime/inbox.mjs +122 -0
- package/lib/engine/session-runtime/notifications.mjs +129 -0
- package/lib/engine/session-runtime/registry.mjs +328 -0
- package/lib/engine/session-runtime/runner.mjs +344 -0
- package/lib/engine/session-runtime/socket.mjs +212 -0
- package/lib/engine/session-runtime/wakeup.mjs +115 -0
- package/lib/engine/skills/index.mjs +321 -0
- package/lib/engine/tools/bash-background.mjs +533 -0
- package/lib/engine/tools/bash.mjs +216 -0
- package/lib/engine/tools/edit.mjs +97 -0
- package/lib/engine/tools/glob.mjs +81 -0
- package/lib/engine/tools/grep.mjs +224 -0
- package/lib/engine/tools/index.mjs +84 -0
- package/lib/engine/tools/list-agents.mjs +32 -0
- package/lib/engine/tools/ls.mjs +127 -0
- package/lib/engine/tools/monitor.mjs +82 -0
- package/lib/engine/tools/notebook-edit.mjs +218 -0
- package/lib/engine/tools/read.mjs +103 -0
- package/lib/engine/tools/schedule-wakeup.mjs +45 -0
- package/lib/engine/tools/schema.mjs +144 -0
- package/lib/engine/tools/send-message.mjs +77 -0
- package/lib/engine/tools/session.mjs +70 -0
- package/lib/engine/tools/todo.mjs +144 -0
- package/lib/engine/tools/toolsearch.mjs +217 -0
- package/lib/engine/tools/walk.mjs +193 -0
- package/lib/engine/tools/web-switch.mjs +31 -0
- package/lib/engine/tools/webfetch-html.mjs +387 -0
- package/lib/engine/tools/webfetch-net.mjs +340 -0
- package/lib/engine/tools/webfetch.mjs +198 -0
- package/lib/engine/tools/websearch.mjs +91 -0
- package/lib/engine/tools/workflow.mjs +95 -0
- package/lib/engine/tools/write.mjs +76 -0
- package/lib/engine/tui/line-editor.mjs +137 -0
- package/lib/engine/tui/render.mjs +86 -0
- package/lib/engine/tui/tui.mjs +274 -0
- package/lib/engine/wire/anthropic-messages.mjs +263 -0
- package/lib/engine/wire/effort.mjs +36 -0
- package/lib/engine/wire/errors.mjs +496 -0
- package/lib/engine/wire/http.mjs +441 -0
- package/lib/engine/wire/index.mjs +76 -0
- package/lib/engine/wire/openai-chat.mjs +332 -0
- package/lib/engine/wire/prompt-cache.mjs +79 -0
- package/lib/engine/wire/search.mjs +140 -0
- package/lib/engine/wire/sse.mjs +114 -0
- package/lib/engine/wire/stall.mjs +349 -0
- package/lib/engine/wire/token-provider.mjs +175 -0
- package/lib/engine/wire/usage.mjs +192 -0
- package/lib/engine/workflow/host.mjs +524 -0
- package/lib/engine/workflow/journal.mjs +188 -0
- package/lib/engine/workflow/json-schema.mjs +171 -0
- package/lib/engine/workflow/meta.mjs +329 -0
- package/lib/engine/workflow/notifications.mjs +52 -0
- package/lib/engine/workflow/runtime.mjs +447 -0
- package/lib/engine/workflow/sandbox.mjs +534 -0
- package/lib/engine/workflow/worker.mjs +141 -0
- package/lib/engine/workflow/worktree.mjs +74 -0
- package/lib/execution/disposition.mjs +1 -1
- package/lib/execution/intake.mjs +10 -0
- package/lib/execution/surface-policy.mjs +15 -0
- package/lib/learning/curator.mjs +8 -6
- package/lib/learning/reflect.mjs +8 -6
- package/lib/model-router/catalog/cohort.yaml +137 -0
- package/lib/model-router/catalog.mjs +118 -1
- package/lib/model-router/failover.mjs +67 -16
- package/lib/model-router/llm-task.mjs +39 -3
- package/lib/model-router/resolve.mjs +89 -3
- package/lib/model-router/spawn.mjs +46 -47
- package/lib/model-router/taxonomy.mjs +126 -4
- package/lib/org/cost-sync.mjs +141 -11
- package/lib/org/inbound/broadcast.mjs +289 -0
- package/lib/org/inbound/collective.mjs +375 -0
- package/lib/org/inbound/directedness.mjs +96 -8
- package/lib/org/inbound/facts.mjs +78 -2
- package/lib/org/inbound/project.mjs +22 -0
- package/lib/org/inbound/surfaces.mjs +14 -0
- package/lib/org/llm-token.mjs +879 -0
- package/lib/org/mesh.mjs +61 -0
- package/lib/org/messaging.mjs +3 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +15 -0
- package/lib/org/quota.mjs +520 -0
- package/lib/org/tool-surface.mjs +104 -16
- package/lib/org/ui-parity.mjs +16 -1
- package/lib/org/work-ledger.mjs +37 -6
- package/lib/rate-guard.mjs +114 -1
- package/lib/resource-governor.mjs +41 -6
- package/lib/runtime/adapter.mjs +823 -0
- package/lib/runtime/child-env.mjs +191 -0
- package/lib/runtime/legacy-shell-guard.mjs +97 -0
- package/lib/runtime/seat-engine.mjs +162 -0
- package/lib/session/ask-ledger.mjs +271 -0
- package/lib/session/current-work.mjs +676 -0
- package/lib/session/feed-core.mjs +40 -3
- package/lib/session/launch-args.mjs +56 -4
- package/lib/session/status-summary.mjs +26 -9
- package/lib/session/upgrade-notice.mjs +42 -0
- package/lib/setup/claude-probe.mjs +117 -13
- package/lib/setup/enrich.mjs +13 -10
- package/lib/setup/sections/model.mjs +39 -13
- package/lib/telemetry/collect.mjs +208 -9
- package/lib/upgrade/ignored-drift.mjs +105 -0
- package/lib/voice/post-call-brief.mjs +30 -17
- package/package.json +13 -3
- package/plugins/maestro-skills/skills/board-work.md +5 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
- package/plugins/maestro-skills/skills/main-session.md +18 -7
- package/scripts/ci/check-tarball-fidelity.mjs +126 -2
- package/scripts/ci/run-tests.mjs +47 -19
- package/scripts/cohort-llm/api-key-helper.mjs +92 -0
- package/scripts/collective/hook-runner.mjs +29 -2
- package/scripts/continuous-monitor.sh +13 -0
- package/scripts/cost/track-claude-usage.mjs +15 -0
- package/scripts/daemon/agent-daemon.mjs +408 -20
- package/scripts/daemon/assurance.mjs +48 -12
- package/scripts/daemon/cadence-consumer.mjs +218 -68
- package/scripts/daemon/cadence-handlers.mjs +73 -4
- package/scripts/daemon/classifier.mjs +75 -26
- package/scripts/daemon/context-compiler.mjs +51 -37
- package/scripts/daemon/deliver.mjs +30 -1
- package/scripts/daemon/dispatcher.mjs +595 -149
- package/scripts/daemon/health.mjs +14 -1
- package/scripts/daemon/maestro-daemon.mjs +11 -0
- package/scripts/daemon/prompt-builder.mjs +24 -0
- package/scripts/daemon/responder.mjs +246 -79
- package/scripts/daemon/sdk-version.mjs +98 -16
- package/scripts/eval/probe-gateway.mjs +635 -0
- package/scripts/eval/replay/extract.mjs +270 -0
- package/scripts/eval/replay/grade.mjs +260 -0
- package/scripts/eval/replay/lib/config.mjs +50 -0
- package/scripts/eval/replay/lib/effects.mjs +65 -0
- package/scripts/eval/replay/lib/fixture.mjs +188 -0
- package/scripts/eval/replay/lib/judge.mjs +72 -0
- package/scripts/eval/replay/lib/redact.mjs +136 -0
- package/scripts/eval/replay/lib/sandbox.mjs +170 -0
- package/scripts/eval/replay/lib/schema-check.mjs +63 -0
- package/scripts/eval/replay/lib/transcript.mjs +76 -0
- package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
- package/scripts/eval/replay/report.mjs +185 -0
- package/scripts/eval/replay/run.mjs +404 -0
- package/scripts/fleet/rollout.mjs +1094 -0
- package/scripts/hooks/pre-send-audit.sh +36 -245
- package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
- package/scripts/hooks/validate-state-yaml.sh +190 -0
- package/scripts/huddle/huddle-llm.mjs +361 -0
- package/scripts/huddle/huddle-server.mjs +46 -121
- package/scripts/local-triggers/autoupdate.sh +448 -78
- package/scripts/local-triggers/run-trigger.sh +13 -0
- package/scripts/maintenance/pin-integrity.mjs +364 -0
- package/scripts/poll-slack-events.sh +41 -9
- package/scripts/poller/slack-socket-mode.mjs +28 -3
- package/scripts/session/supervisor.mjs +80 -13
- package/scripts/spawn-session.sh +13 -0
- package/bin/maestro.test.mjs +0 -1574
- package/lib/action-executor.test.mjs +0 -871
- package/lib/archetype.test.mjs +0 -132
- package/lib/assurance/plan-note.test.mjs +0 -234
- package/lib/assurance/room-budget.test.mjs +0 -486
- package/lib/assurance/tier.test.mjs +0 -174
- package/lib/autonomy.test.mjs +0 -66
- package/lib/backlog.test.mjs +0 -302
- package/lib/backup/policy.test.mjs +0 -305
- package/lib/budget-escalate.test.mjs +0 -232
- package/lib/budget-guard.envelope.test.mjs +0 -476
- package/lib/budget-guard.test.mjs +0 -427
- package/lib/cadence-bus-requeue.test.mjs +0 -83
- package/lib/cadence-bus-schedule.test.mjs +0 -194
- package/lib/cadence-bus.test.mjs +0 -720
- package/lib/cadences.test.mjs +0 -230
- package/lib/capability/inventory.test.mjs +0 -232
- package/lib/capability.test.mjs +0 -78
- package/lib/channels/base-adapter.test.mjs +0 -590
- package/lib/channels/channels.test.mjs +0 -371
- package/lib/channels/contract.test.mjs +0 -162
- package/lib/channels/inbox-item.test.mjs +0 -368
- package/lib/channels/orgmail/adapter.test.mjs +0 -448
- package/lib/channels/pairing.test.mjs +0 -270
- package/lib/channels/repeat-suppressor.test.mjs +0 -134
- package/lib/channels/slack-adapter.test.mjs +0 -212
- package/lib/channels/telegram-adapter.test.mjs +0 -306
- package/lib/channels/voice/adapter.test.mjs +0 -278
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
- package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
- package/lib/charter.test.mjs +0 -89
- package/lib/claude-bin.test.mjs +0 -131
- package/lib/cli/board.test.mjs +0 -227
- package/lib/cli/design.test.mjs +0 -270
- package/lib/cli/doctor-checks.test.mjs +0 -336
- package/lib/cli/global-setup-extras.test.mjs +0 -462
- package/lib/cli/inbox.test.mjs +0 -230
- package/lib/cli/session-ack.test.mjs +0 -63
- package/lib/cli/session.test.mjs +0 -613
- package/lib/collective/capture.test.mjs +0 -121
- package/lib/collective/cards.test.mjs +0 -114
- package/lib/collective/config.test.mjs +0 -123
- package/lib/collective/global-config.test.mjs +0 -220
- package/lib/collective/global-skills.test.mjs +0 -126
- package/lib/collective/presence.test.mjs +0 -95
- package/lib/collective/recall.test.mjs +0 -116
- package/lib/collective/vendor-skills.test.mjs +0 -306
- package/lib/comms/send-gate.test.mjs +0 -770
- package/lib/comms.test.mjs +0 -41
- package/lib/context/budget.test.mjs +0 -252
- package/lib/context/history-scope.test.mjs +0 -79
- package/lib/cost/ledger-row.test.mjs +0 -183
- package/lib/design/design-md.test.mjs +0 -318
- package/lib/design/fixtures/DESIGN.golden.md +0 -238
- package/lib/design/fixtures/PRODUCT.golden.md +0 -67
- package/lib/design/fixtures/foundation.json +0 -133
- package/lib/design/refresh-gate.test.mjs +0 -144
- package/lib/design/write.test.mjs +0 -241
- package/lib/diagnostics/alerts.test.mjs +0 -318
- package/lib/diagnostics/backup-freshness.test.mjs +0 -185
- package/lib/diagnostics/counters.test.mjs +0 -206
- package/lib/diagnostics/events.test.mjs +0 -290
- package/lib/diagnostics/otel.test.mjs +0 -196
- package/lib/diagnostics/trace.test.mjs +0 -251
- package/lib/env-compat.test.mjs +0 -104
- package/lib/execution/disposition.test.mjs +0 -553
- package/lib/execution/drive.test.mjs +0 -270
- package/lib/execution/effects.test.mjs +0 -344
- package/lib/execution/intake.test.mjs +0 -389
- package/lib/execution/journal.test.mjs +0 -261
- package/lib/execution/match.test.mjs +0 -235
- package/lib/execution/pipeline.test.mjs +0 -392
- package/lib/execution/route.test.mjs +0 -186
- package/lib/execution/surface-policy.test.mjs +0 -162
- package/lib/fs-atomic.test.mjs +0 -72
- package/lib/fs-ownership.test.mjs +0 -158
- package/lib/goals/admission.test.mjs +0 -164
- package/lib/goals/classify.test.mjs +0 -167
- package/lib/goals/collaborate.test.mjs +0 -336
- package/lib/goals/gaps.test.mjs +0 -284
- package/lib/goals/loop.test.mjs +0 -845
- package/lib/hooks/bus.test.mjs +0 -387
- package/lib/identity/persona.test.mjs +0 -142
- package/lib/kpi-sensors.test.mjs +0 -278
- package/lib/kpi.test.mjs +0 -244
- package/lib/learning/config.test.mjs +0 -75
- package/lib/learning/counters.test.mjs +0 -69
- package/lib/learning/curator-consolidate.test.mjs +0 -238
- package/lib/learning/curator.test.mjs +0 -106
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.test.mjs +0 -125
- package/lib/learning/skill-writer.test.mjs +0 -210
- package/lib/mandate/audit.test.mjs +0 -195
- package/lib/mandate/contract.test.mjs +0 -185
- package/lib/mandate/derive.test.mjs +0 -274
- package/lib/mandate/model.test.mjs +0 -164
- package/lib/mandate/refresh.test.mjs +0 -389
- package/lib/mcp/server.test.mjs +0 -426
- package/lib/model-router/auth-profiles.test.mjs +0 -580
- package/lib/model-router/catalog.test.mjs +0 -385
- package/lib/model-router/economics.test.mjs +0 -438
- package/lib/model-router/failover.test.mjs +0 -439
- package/lib/model-router/health.test.mjs +0 -338
- package/lib/model-router/integration-coverage.test.mjs +0 -831
- package/lib/model-router/integration.test.mjs +0 -564
- package/lib/model-router/ledger.test.mjs +0 -415
- package/lib/model-router/llm-task.test.mjs +0 -392
- package/lib/model-router/org-credentials.test.mjs +0 -265
- package/lib/model-router/pricing-refresh.test.mjs +0 -286
- package/lib/model-router/reconcile.test.mjs +0 -316
- package/lib/model-router/repair.test.mjs +0 -180
- package/lib/model-router/spawn.test.mjs +0 -446
- package/lib/model-router/taxonomy.test.mjs +0 -410
- package/lib/model-router.test.mjs +0 -1207
- package/lib/org/activity.test.mjs +0 -134
- package/lib/org/approvals.test.mjs +0 -216
- package/lib/org/awareness.test.mjs +0 -159
- package/lib/org/board-mine-cache.test.mjs +0 -53
- package/lib/org/board.test.mjs +0 -187
- package/lib/org/bootstrap-context.test.mjs +0 -153
- package/lib/org/client.test.mjs +0 -1206
- package/lib/org/cohort-client.test.mjs +0 -126
- package/lib/org/cost-sync.test.mjs +0 -153
- package/lib/org/doctor.test.mjs +0 -346
- package/lib/org/engagement-ledger.test.mjs +0 -112
- package/lib/org/engagement.test.mjs +0 -739
- package/lib/org/handoff.test.mjs +0 -269
- package/lib/org/inbound/directedness.test.mjs +0 -668
- package/lib/org/inbound/facts.test.mjs +0 -471
- package/lib/org/inbound/hydrate.test.mjs +0 -908
- package/lib/org/inbound/index.test.mjs +0 -429
- package/lib/org/inbound/project.test.mjs +0 -287
- package/lib/org/integration-tools.test.mjs +0 -160
- package/lib/org/keys.test.mjs +0 -92
- package/lib/org/knowledge.test.mjs +0 -326
- package/lib/org/leases.test.mjs +0 -235
- package/lib/org/mesh-directives.test.mjs +0 -110
- package/lib/org/mesh-integration.test.mjs +0 -127
- package/lib/org/mesh.test.mjs +0 -400
- package/lib/org/messaging.test.mjs +0 -471
- package/lib/org/param-contract.test.mjs +0 -477
- package/lib/org/policy.test.mjs +0 -237
- package/lib/org/protocol.checksum.test.mjs +0 -90
- package/lib/org/protocol.test.mjs +0 -323
- package/lib/org/push.test.mjs +0 -792
- package/lib/org/registry.test.mjs +0 -100
- package/lib/org/resource-tools.test.mjs +0 -361
- package/lib/org/tool-access.test.mjs +0 -144
- package/lib/org/tool-surface-integration.test.mjs +0 -120
- package/lib/org/tool-surface.test.mjs +0 -1268
- package/lib/org/typing.test.mjs +0 -291
- package/lib/org/ui-parity.test.mjs +0 -560
- package/lib/org/verify.test.mjs +0 -194
- package/lib/org/work-ledger.test.mjs +0 -273
- package/lib/plan/adoption-e2e.test.mjs +0 -366
- package/lib/plan/budget-enforcement.test.mjs +0 -400
- package/lib/plan/compile.test.mjs +0 -382
- package/lib/plan/emit.test.mjs +0 -269
- package/lib/plan/explain.test.mjs +0 -188
- package/lib/prompts/parallelism.test.mjs +0 -177
- package/lib/rag/rag.test.mjs +0 -505
- package/lib/rate-guard.test.mjs +0 -272
- package/lib/reactive-gate.test.mjs +0 -57
- package/lib/render.test.mjs +0 -68
- package/lib/resource-governor.test.mjs +0 -488
- package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
- package/lib/scheduling/jitter.test.mjs +0 -140
- package/lib/secrets/broker.test.mjs +0 -280
- package/lib/secrets/providers.test.mjs +0 -274
- package/lib/security/audit-engine.test.mjs +0 -424
- package/lib/security/coerce-args.test.mjs +0 -281
- package/lib/security/dangerous-tools.test.mjs +0 -68
- package/lib/security/external-content.test.mjs +0 -84
- package/lib/security/redact.test.mjs +0 -441
- package/lib/security/secret-equal.test.mjs +0 -55
- package/lib/session/config.test.mjs +0 -92
- package/lib/session/feed-core.test.mjs +0 -198
- package/lib/session/first-run.test.mjs +0 -121
- package/lib/session/frontdoor.test.mjs +0 -205
- package/lib/session/handoffs.test.mjs +0 -183
- package/lib/session/identity.test.mjs +0 -180
- package/lib/session/inbox-claims.test.mjs +0 -286
- package/lib/session/launch-args.test.mjs +0 -157
- package/lib/session/liveness.test.mjs +0 -100
- package/lib/session/status-summary.test.mjs +0 -118
- package/lib/session-permissions.test.mjs +0 -120
- package/lib/setup/claude-probe.test.mjs +0 -187
- package/lib/setup/completeness.test.mjs +0 -110
- package/lib/setup/context-pack.test.mjs +0 -89
- package/lib/setup/enrich.test.mjs +0 -115
- package/lib/setup/enroll-from-cohort.test.mjs +0 -300
- package/lib/setup/integration.test.mjs +0 -162
- package/lib/setup/io.test.mjs +0 -77
- package/lib/setup/runner.test.mjs +0 -132
- package/lib/setup/sections/identity.test.mjs +0 -234
- package/lib/setup/sections/inventory.test.mjs +0 -198
- package/lib/setup/sections/learning.test.mjs +0 -81
- package/lib/setup/sections/mandate.test.mjs +0 -388
- package/lib/setup/sections/messaging.test.mjs +0 -127
- package/lib/setup/sections/model.test.mjs +0 -240
- package/lib/setup/sections/org.test.mjs +0 -346
- package/lib/setup/sections/orgmail.test.mjs +0 -118
- package/lib/setup/sections/recovery.test.mjs +0 -98
- package/lib/setup/sections/subagents.test.mjs +0 -429
- package/lib/setup/sections/verify.test.mjs +0 -175
- package/lib/setup/sot.test.mjs +0 -81
- package/lib/setup/state.test.mjs +0 -115
- package/lib/singleton.test.mjs +0 -151
- package/lib/subagents/cli.test.mjs +0 -389
- package/lib/subagents/client.test.mjs +0 -309
- package/lib/subagents/gap.test.mjs +0 -234
- package/lib/subagents/lock.test.mjs +0 -248
- package/lib/subagents/manifest.test.mjs +0 -175
- package/lib/subagents/refs.test.mjs +0 -204
- package/lib/subagents/resolve.test.mjs +0 -422
- package/lib/subagents/schema.test.mjs +0 -328
- package/lib/telemetry/alerts.test.mjs +0 -109
- package/lib/telemetry/collect.test.mjs +0 -1274
- package/lib/tool-definitions-integration.test.mjs +0 -83
- package/lib/tool-definitions.test.mjs +0 -437
- package/lib/upgrade/global-refresh.test.mjs +0 -65
- package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
- package/lib/upgrade/post-steps.test.mjs +0 -200
- package/lib/upgrade/verify.test.mjs +0 -164
- package/lib/util/fetch-timeout.test.mjs +0 -202
- package/lib/util/reconnect.test.mjs +0 -369
- package/lib/util/unhandled.test.mjs +0 -216
- package/lib/voice/outbound.test.mjs +0 -69
- package/lib/voice/session-rotation.test.mjs +0 -114
- package/lib/voice/stt.test.mjs +0 -226
- package/lib/voice/voice.test.mjs +0 -990
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
- package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
- package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
- package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
- package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
- package/scripts/ci/check-skill-packs.test.mjs +0 -495
- package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
- package/scripts/ci/check.test.mjs +0 -194
- package/scripts/ci/conformance-org-api.test.mjs +0 -425
- package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
- package/scripts/collective/hook-runner.test.mjs +0 -173
- package/scripts/cost/fleet-digest.test.mjs +0 -207
- package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
- package/scripts/cost/track-claude-usage.test.mjs +0 -148
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
- package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
- package/scripts/daemon/agent-daemon.test.mjs +0 -995
- package/scripts/daemon/assurance-e2e.test.mjs +0 -613
- package/scripts/daemon/assurance.test.mjs +0 -1791
- package/scripts/daemon/board-mirror.test.mjs +0 -165
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
- package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
- package/scripts/daemon/cadence-consumer.test.mjs +0 -776
- package/scripts/daemon/cadence-handlers.test.mjs +0 -837
- package/scripts/daemon/classifier-identity.test.mjs +0 -137
- package/scripts/daemon/classifier.test.mjs +0 -266
- package/scripts/daemon/classify-kind.test.mjs +0 -40
- package/scripts/daemon/context-compiler.test.mjs +0 -406
- package/scripts/daemon/deliver.test.mjs +0 -564
- package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
- package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
- package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
- package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
- package/scripts/daemon/execution-ladder.test.mjs +0 -470
- package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
- package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
- package/scripts/daemon/inbox-deferral.test.mjs +0 -336
- package/scripts/daemon/inbox-wake.test.mjs +0 -199
- package/scripts/daemon/integration.test.mjs +0 -149
- package/scripts/daemon/lib/self-echo.test.mjs +0 -153
- package/scripts/daemon/lib/session-router.test.mjs +0 -554
- package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
- package/scripts/daemon/prompt-builder.test.mjs +0 -556
- package/scripts/daemon/responder-cost.test.mjs +0 -68
- package/scripts/daemon/responder-history.test.mjs +0 -221
- package/scripts/daemon/sdk-version.test.mjs +0 -31
- package/scripts/daemon/session-lock.test.mjs +0 -252
- package/scripts/daemon/session-outcomes.test.mjs +0 -533
- package/scripts/daemon/typing-registry.test.mjs +0 -102
- package/scripts/hooks/pre-send-audit.test.mjs +0 -354
- package/scripts/huddle/huddle-prompt.test.mjs +0 -176
- package/scripts/local-triggers/autoupdate.test.mjs +0 -518
- package/scripts/local-triggers/generate-plists.test.mjs +0 -456
- package/scripts/media-generation/brand-clause.test.mjs +0 -135
- package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
- package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
- package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
- package/scripts/poller/slack-socket-mode.test.mjs +0 -805
- package/scripts/poller-launchd/install.test.mjs +0 -243
- package/scripts/restore-from-backup.test.mjs +0 -181
- package/scripts/session/feed.test.mjs +0 -196
- package/scripts/session/supervisor-sh.test.mjs +0 -218
- package/scripts/session/supervisor.test.mjs +0 -482
- package/scripts/setup/configure-macos.test.mjs +0 -306
- package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
- package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
- package/scripts/setup/generate-capability.test.mjs +0 -134
- package/scripts/setup/init-agent.test.mjs +0 -370
- package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
- package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
- package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
package/lib/comms/send-gate.mjs
CHANGED
|
@@ -47,6 +47,12 @@
|
|
|
47
47
|
* any) on stdout. Any unexpected internal error for an EXTERNAL recipient also
|
|
48
48
|
* exits 1 (fail closed); for an internal recipient it exits 0 (fail open).
|
|
49
49
|
*
|
|
50
|
+
* Hook form (the Claude Code PreToolUse gate, via scripts/hooks/pre-send-audit.sh)
|
|
51
|
+
* ---------------------------------------------------------------------------
|
|
52
|
+
* <PreToolUse JSON> | node lib/comms/send-gate.mjs --pre-send-hook <channel>
|
|
53
|
+
* exit 0 → allow (audit line on stdout); exit 2 → BLOCK (reason on stderr).
|
|
54
|
+
* Never exit 1: Claude Code does not block on it. See runPreSendHook.
|
|
55
|
+
*
|
|
50
56
|
* @module lib/comms/send-gate
|
|
51
57
|
*/
|
|
52
58
|
|
|
@@ -55,8 +61,12 @@
|
|
|
55
61
|
import {
|
|
56
62
|
readFileSync as nodeReadFileSync,
|
|
57
63
|
existsSync as nodeExistsSync,
|
|
64
|
+
statSync as nodeStatSync,
|
|
65
|
+
mkdirSync as nodeMkdirSync,
|
|
66
|
+
writeSync as nodeWriteSync,
|
|
58
67
|
} from "node:fs";
|
|
59
68
|
import { join } from "node:path";
|
|
69
|
+
import { writeFileAtomic, appendJsonl } from "../fs-atomic.mjs";
|
|
60
70
|
|
|
61
71
|
/* ─────────────────────────── policy file locations ───────────────────────── */
|
|
62
72
|
|
|
@@ -117,6 +127,51 @@ const PERSONA_FILES = Object.freeze({
|
|
|
117
127
|
peers: "state/session/peers.json",
|
|
118
128
|
});
|
|
119
129
|
|
|
130
|
+
/* ──────────────────────── session-scaffold markers ───────────────────────── */
|
|
131
|
+
// A raw model turn must never reach a person. On 5 and 20 September 2026, bodies
|
|
132
|
+
// carrying `<user_message>`, a closing `</thinking>` and the responder's
|
|
133
|
+
// `[auto-reply: "…"]` preamble were posted verbatim into live Cohort channels —
|
|
134
|
+
// in the worst instance a triage conclusion that no reply was warranted,
|
|
135
|
+
// published AS the reply. Root cause was scripts/daemon/responder.mjs: BOTH the
|
|
136
|
+
// success path and the JSON-parse fallback resolve `cliResult.text`, and nothing
|
|
137
|
+
// sat between that and the wire (#64 closes the fallback). Patching the fallback
|
|
138
|
+
// closes one mouth; this closes the throat.
|
|
139
|
+
//
|
|
140
|
+
// Scope: the SCAFFOLD ONLY — wrappers a harness emits around a turn. Deliberately
|
|
141
|
+
// not a general "looks like XML" test, because org bodies legitimately carry angle
|
|
142
|
+
// brackets (diffs, HTML fragments, Slack's `<@U…>` mention syntax, `NAV < 1bp`).
|
|
143
|
+
//
|
|
144
|
+
// Matching is case-insensitive over the body with INLINE code spans REMOVED
|
|
145
|
+
// (see stripCodeSpans). A marker inside inline backticks is a citation, not a
|
|
146
|
+
// leak, and the incident thread that diagnosed this defect quoted every one of
|
|
147
|
+
// these in backticks. Without that carve-out the first thing this rule blocks
|
|
148
|
+
// is its own post-mortem, and the second is somebody switching the gate off to
|
|
149
|
+
// send it. Fenced (```) blocks are NOT exempt — a fenced block is also how a
|
|
150
|
+
// seat pastes a templated message or quotes a draft for review, so carving it
|
|
151
|
+
// out would reopen the exact leak this rule exists to stop.
|
|
152
|
+
//
|
|
153
|
+
// ONE list, shared by both screening paths in this module (`screenOutbound` for
|
|
154
|
+
// the in-process lanes, `screenHookPayload` for the PreToolUse hook). The ported
|
|
155
|
+
// hook rules are duplicated as HOOK_* constants because they owe shell parity;
|
|
156
|
+
// this rule is new, owes no parity, and must not be duplicated.
|
|
157
|
+
export const SCAFFOLD_MARKERS = Object.freeze([
|
|
158
|
+
"<user_message>",
|
|
159
|
+
"</user_message>",
|
|
160
|
+
"<thinking>",
|
|
161
|
+
"</thinking>",
|
|
162
|
+
"<system-reminder>",
|
|
163
|
+
"</system-reminder>",
|
|
164
|
+
"<function_calls>",
|
|
165
|
+
"</function_calls>",
|
|
166
|
+
"<function_results>",
|
|
167
|
+
"</function_results>",
|
|
168
|
+
"<invoke name=",
|
|
169
|
+
"</invoke>",
|
|
170
|
+
"[auto-reply:",
|
|
171
|
+
"<<<UNTRUSTED_",
|
|
172
|
+
"<END_UNTRUSTED_",
|
|
173
|
+
]);
|
|
174
|
+
|
|
120
175
|
/* ────────────────────────── recipient classification ─────────────────────── */
|
|
121
176
|
|
|
122
177
|
/**
|
|
@@ -365,6 +420,62 @@ function personaContext(fs, agentRoot, { agentFirst, sessionNames }) {
|
|
|
365
420
|
return { first, sessionNames: names };
|
|
366
421
|
}
|
|
367
422
|
|
|
423
|
+
/**
|
|
424
|
+
* Remove INLINE code spans only, so a deliberately quoted marker (`` `<thinking>` ``)
|
|
425
|
+
* is not read as a leaked one. Replaced with a space rather than deleted, so
|
|
426
|
+
* stripping can never splice two halves of a marker together across the removed
|
|
427
|
+
* span (`` `<user_` `` followed by `` `message>` `` must not become
|
|
428
|
+
* `<user_message>`).
|
|
429
|
+
*
|
|
430
|
+
* Deliberately does NOT strip fenced (```) blocks: a fenced block isn't
|
|
431
|
+
* obviously a citation of the leak bug — it's also how a seat pastes a
|
|
432
|
+
* templated message or quotes a draft for review — so exempting it from the
|
|
433
|
+
* screen would carve out exactly the failure this rule exists to stop. Quote a
|
|
434
|
+
* marker deliberately with inline backticks; a marker inside a fenced block
|
|
435
|
+
* still blocks (pre-merge review on #66, Ravi Patel).
|
|
436
|
+
*
|
|
437
|
+
* Bounded to a 1-2 backtick run (single line, no fence). The run must not be
|
|
438
|
+
* adjacent to a further backtick on either side, so this can never partially
|
|
439
|
+
* consume a ``` fence delimiter (matching 2 of its 3 backticks and leaving a
|
|
440
|
+
* dangling one) — inside a fence, no width starts or closes a match, so the
|
|
441
|
+
* fence content is left untouched for screenScaffold to see.
|
|
442
|
+
*
|
|
443
|
+
* @param {string} text
|
|
444
|
+
* @returns {string}
|
|
445
|
+
*/
|
|
446
|
+
export function stripCodeSpans(text) {
|
|
447
|
+
return String(text || "").replace(/(?<!`)(`{1,2})(?!`)[^\n`]*?(?<!`)\1(?!`)/g, " ");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* (a0) Session-scaffold screen. Pure; no IO. Returns a block reason naming the
|
|
452
|
+
* marker, or null.
|
|
453
|
+
*
|
|
454
|
+
* This blocks for INTERNAL recipients too, and that is the point: every recorded
|
|
455
|
+
* leak went to an internal Cohort channel, and those channels have human members.
|
|
456
|
+
* There is no recipient class for which publishing a raw model turn is
|
|
457
|
+
* acceptable — so unlike the policy-file layers this screen has no fail posture
|
|
458
|
+
* to resolve. It reads no file, so it cannot fail open.
|
|
459
|
+
*
|
|
460
|
+
* @param {string} text
|
|
461
|
+
* @returns {string|null}
|
|
462
|
+
*/
|
|
463
|
+
export function screenScaffold(text) {
|
|
464
|
+
const t = typeof text === "string" ? text : "";
|
|
465
|
+
if (!t) return null;
|
|
466
|
+
const lc = stripCodeSpans(t).toLowerCase();
|
|
467
|
+
for (const marker of SCAFFOLD_MARKERS) {
|
|
468
|
+
if (lc.includes(marker.toLowerCase())) {
|
|
469
|
+
return (
|
|
470
|
+
`session-scaffold marker "${marker}" in the outbound body — a raw model turn ` +
|
|
471
|
+
`must never reach a person; send the reply you meant to write. ` +
|
|
472
|
+
`(To quote a marker deliberately, put it in backticks.)`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
|
|
368
479
|
/**
|
|
369
480
|
* (b) AI-disclosure screen. Resolves the active posture for the recipient's
|
|
370
481
|
* jurisdiction from ai-disclosure.yaml and enforces two things:
|
|
@@ -586,6 +697,14 @@ export async function screenOutbound({
|
|
|
586
697
|
// file is unreadable we still apply the embedded constant list (the SoT
|
|
587
698
|
// is mirrored here), so a missing style file does NOT open the AI-tell
|
|
588
699
|
// gate; it only loses the ability to refresh the list.
|
|
700
|
+
// (a0) Session scaffold — FIRST, ahead of every other layer. A dumped model
|
|
701
|
+
// turn very often also trips the banned-phrase list, and if that fires
|
|
702
|
+
// first the operator reads "banned AI-tell phrase" and goes off to fix
|
|
703
|
+
// the wording of a message that should never have had a body at all.
|
|
704
|
+
// The most severe diagnosis has to be the one that gets reported.
|
|
705
|
+
const scaffoldReason = screenScaffold(text);
|
|
706
|
+
if (scaffoldReason) return blocked(scaffoldReason);
|
|
707
|
+
|
|
589
708
|
const bannedReason = screenBannedPhrases(text);
|
|
590
709
|
if (bannedReason) return blocked(bannedReason);
|
|
591
710
|
|
|
@@ -660,6 +779,443 @@ export async function screenOutbound({
|
|
|
660
779
|
}
|
|
661
780
|
}
|
|
662
781
|
|
|
782
|
+
/* ──────────────────── pre-send audit hook (ported from shell) ─────────────── */
|
|
783
|
+
//
|
|
784
|
+
// scripts/hooks/pre-send-audit.sh used to carry these rules in bash + jq + grep;
|
|
785
|
+
// it is now a thin wrapper that runs `node lib/comms/send-gate.mjs
|
|
786
|
+
// --pre-send-hook <channel>`. The rules live here so the Claude Code PreToolUse
|
|
787
|
+
// hook and a native engine gate (lib/engine, W2) share one implementation — no
|
|
788
|
+
// window in which outbound sends are gated by a copy that drifted.
|
|
789
|
+
//
|
|
790
|
+
// The port reproduces the shell verdicts, reasons and side effects exactly
|
|
791
|
+
// over the hook's test corpus (scripts/hooks/pre-send-audit.parity.test.mjs
|
|
792
|
+
// runs every case against the frozen shell copy and this module). Where bash/jq
|
|
793
|
+
// semantics were an accident rather than a rule, the port is deliberately no
|
|
794
|
+
// LESS strict, and says so inline: a multi-document JSON payload is malformed,
|
|
795
|
+
// whitespace inside a phrase includes non-ASCII spaces, a recipient with an
|
|
796
|
+
// embedded newline can never match an allowlist line.
|
|
797
|
+
//
|
|
798
|
+
// Split into pure verdict functions (screenHookPayload, applyHookRateCaps, and
|
|
799
|
+
// the extract/resolve helpers under them) and one effect edge (runPreSendHook)
|
|
800
|
+
// that reads identity/allowlist/counter files, writes the counter and the send
|
|
801
|
+
// log, and takes `now` as a parameter.
|
|
802
|
+
|
|
803
|
+
/** Banned substrings in the hook's order: the in-process list plus "Great question!" anywhere. */
|
|
804
|
+
export const HOOK_BANNED_SUBSTRINGS = Object.freeze([
|
|
805
|
+
...BANNED_SUBSTRINGS.slice(0, 8),
|
|
806
|
+
"Great question!",
|
|
807
|
+
...BANNED_SUBSTRINGS.slice(8),
|
|
808
|
+
]);
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* The persona patterns, compiled the way the hook matched them: grep is
|
|
812
|
+
* line-bounded, so whitespace inside a phrase never spans a newline.
|
|
813
|
+
*/
|
|
814
|
+
export const HOOK_PERSONA_PATTERNS = Object.freeze(
|
|
815
|
+
PERSONA_PATTERNS.map((re) => new RegExp(re.source.replace(/\\s/g, "[^\\S\\n]"), re.flags)),
|
|
816
|
+
);
|
|
817
|
+
|
|
818
|
+
/** Filler openers at the start of any line, or after a control char (grep's [[:cntrl:]]). */
|
|
819
|
+
const HOOK_OPENER_RE = /(^|[\x00-\x1f\x7f])[^\S\n]*(certainly!|absolutely!|great question!)/im;
|
|
820
|
+
|
|
821
|
+
const HOOK_CHANNEL_REQUIRED = "Channel type required (slack|slack-draft|gmail|whatsapp|cohort)";
|
|
822
|
+
|
|
823
|
+
function isPlainObject(v) {
|
|
824
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/** jq's `//`: only null and false (and a missing key) are "absent". */
|
|
828
|
+
function jqPresent(v) {
|
|
829
|
+
return v !== undefined && v !== null && v !== false;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/** jq `tostring` / `tojson` for the values a payload can carry. */
|
|
833
|
+
function jqString(v) {
|
|
834
|
+
return typeof v === "string" ? v : JSON.stringify(v);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/** `tr '[:upper:]' '[:lower:]'` / jq `ascii_downcase`: ASCII letters only. */
|
|
838
|
+
function asciiLower(s) {
|
|
839
|
+
return String(s).replace(/[A-Z]/g, (c) => c.toLowerCase());
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** Command substitution drops NUL bytes and strips trailing newlines. */
|
|
843
|
+
function shellCaptured(s) {
|
|
844
|
+
return String(s).replace(/\0/g, "").replace(/\n+$/, "");
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* The outbound text of a PreToolUse payload, as the hook concatenated it: from
|
|
849
|
+
* `tool_input`, then `tool_input.params` (org_rpc), then the top level, the
|
|
850
|
+
* fields text/message/body/bodyText/subject/content (non-strings as JSON) and
|
|
851
|
+
* `blocks` as JSON, empties dropped, joined by newlines. Pure.
|
|
852
|
+
*
|
|
853
|
+
* @param {unknown} payload parsed JSON
|
|
854
|
+
* @returns {{ok:true, text:string}|{ok:false}} ok:false where jq errored (a non-object payload)
|
|
855
|
+
*/
|
|
856
|
+
export function extractHookContent(payload) {
|
|
857
|
+
if (!isPlainObject(payload)) return { ok: false };
|
|
858
|
+
const ti = payload.tool_input;
|
|
859
|
+
const sources = [jqPresent(ti) ? ti : {}, isPlainObject(ti) && isPlainObject(ti.params) ? ti.params : {}, payload];
|
|
860
|
+
const parts = [];
|
|
861
|
+
for (const src of sources) {
|
|
862
|
+
if (!isPlainObject(src)) continue; // `.text?` on a non-object yields nothing
|
|
863
|
+
for (const k of ["text", "message", "body", "bodyText", "subject", "content"]) {
|
|
864
|
+
if (jqPresent(src[k])) parts.push(jqString(src[k]));
|
|
865
|
+
}
|
|
866
|
+
const b = src.blocks;
|
|
867
|
+
if (b !== null && typeof b === "object") parts.push(JSON.stringify(b));
|
|
868
|
+
}
|
|
869
|
+
return { ok: true, text: shellCaptured(parts.filter((p) => p !== "").join("\n")) };
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* The recipient the allowlist is checked against: tool_input.to / recipient /
|
|
874
|
+
* channel / email / thread_ts, then top-level to / recipient; non-strings as
|
|
875
|
+
* JSON; "" when none. Pure.
|
|
876
|
+
* @param {object} payload parsed JSON object
|
|
877
|
+
* @returns {string}
|
|
878
|
+
*/
|
|
879
|
+
export function extractHookRecipient(payload) {
|
|
880
|
+
const ti = payload && payload.tool_input;
|
|
881
|
+
const t = jqPresent(ti) ? ti : {};
|
|
882
|
+
const pick = (o, k) => (isPlainObject(o) ? o[k] : undefined);
|
|
883
|
+
const chain = [pick(t, "to"), pick(t, "recipient"), pick(t, "channel"), pick(t, "email"), pick(t, "thread_ts"), pick(payload, "to"), pick(payload, "recipient")];
|
|
884
|
+
const v = chain.find(jqPresent);
|
|
885
|
+
return v === undefined ? "" : shellCaptured(jqString(v));
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* The agent's first name for the `<first>-main` check, resolved as the hook
|
|
890
|
+
* did: MAESTRO_AGENT_FIRST, else the first candidate file (AGENT_ROOT and
|
|
891
|
+
* AGENT_DIR config/agent.json, ~/.claude/maestro-agent.json) whose
|
|
892
|
+
* firstName / agentName / fullName yields a first word; lower-cased and
|
|
893
|
+
* reduced to [a-z0-9-]. Pure over the file TEXTS (null = not a readable file).
|
|
894
|
+
*
|
|
895
|
+
* @param {{envFirst?:string, candidates:Array<string|null>}} o
|
|
896
|
+
* @returns {string} "" when none resolves (the name check is then skipped)
|
|
897
|
+
*/
|
|
898
|
+
export function hookFirstName({ envFirst, candidates = [] }) {
|
|
899
|
+
let first = typeof envFirst === "string" ? envFirst : "";
|
|
900
|
+
if (!first) {
|
|
901
|
+
for (const text of candidates) {
|
|
902
|
+
if (text == null) continue;
|
|
903
|
+
let raw = "";
|
|
904
|
+
try {
|
|
905
|
+
const doc = JSON.parse(text);
|
|
906
|
+
if (isPlainObject(doc)) {
|
|
907
|
+
const v = [doc.firstName, doc.agentName, doc.fullName].find(jqPresent);
|
|
908
|
+
const s = v === undefined ? "" : jqString(v);
|
|
909
|
+
// jq: `"" | split(" ")[0]` is null and ascii_downcase then errors → "".
|
|
910
|
+
raw = s === "" ? "" : asciiLower(s.split(" ")[0]);
|
|
911
|
+
}
|
|
912
|
+
} catch { raw = ""; /* unparseable agent file: jq failed, try the next candidate */ }
|
|
913
|
+
raw = shellCaptured(raw);
|
|
914
|
+
if (raw && raw !== "null") { first = raw; break; }
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return asciiLower(first).replace(/[^a-z0-9-]/g, "");
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Internal session names: `<first>-main`, the registered peers (peers.json as
|
|
922
|
+
* an array or {peers:[…]}), then MAESTRO_SESSION_NAMES (comma-separated, first
|
|
923
|
+
* line). A corrupt registry is an empty one. Pure over the registry TEXT.
|
|
924
|
+
*
|
|
925
|
+
* @param {{first?:string, peersText?:string|null, envNames?:string}} o
|
|
926
|
+
* @returns {string[]}
|
|
927
|
+
*/
|
|
928
|
+
export function hookSessionNames({ first, peersText, envNames }) {
|
|
929
|
+
const names = [];
|
|
930
|
+
if (first) names.push(`${first}-main`);
|
|
931
|
+
if (peersText != null) {
|
|
932
|
+
let doc;
|
|
933
|
+
try { doc = JSON.parse(peersText); } catch { doc = undefined; /* corrupt registry = empty */ }
|
|
934
|
+
let list = [];
|
|
935
|
+
if (Array.isArray(doc)) list = doc;
|
|
936
|
+
else if (isPlainObject(doc)) list = jqPresent(doc.peers) ? doc.peers : [];
|
|
937
|
+
const items = Array.isArray(list) ? list : isPlainObject(list) ? Object.values(list) : [];
|
|
938
|
+
for (const it of items) {
|
|
939
|
+
if (!isPlainObject(it) || !jqPresent(it.name)) continue;
|
|
940
|
+
// `while IFS= read -r pn`: one name per output line, whitespace KEPT
|
|
941
|
+
// (IFS is empty, so read trims nothing); empty lines are skipped.
|
|
942
|
+
for (const line of asciiLower(jqString(it.name)).split("\n")) {
|
|
943
|
+
if (line) names.push(line);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (typeof envNames === "string" && envNames) {
|
|
948
|
+
for (const part of envNames.split("\n")[0].split(",")) {
|
|
949
|
+
const n = asciiLower(part.replace(/[ \t\n\v\f\r]/g, ""));
|
|
950
|
+
if (n) names.push(n);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return names;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/** Word-bounded (on [A-Za-z0-9_-]) session-name hit, trimmed of its boundary chars. */
|
|
957
|
+
function hookSessionNameHit(text, name) {
|
|
958
|
+
const re = new RegExp(`(^|[^A-Za-z0-9_\\n-])${escapeRe(name)}([^A-Za-z0-9_\\n-]|$)`, "im");
|
|
959
|
+
const m = re.exec(text);
|
|
960
|
+
if (!m) return null;
|
|
961
|
+
return m[0].replace(/^[^A-Za-z0-9_-]/, "").replace(/[^A-Za-z0-9_-]$/, "");
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Is `recipient` a line of the allowlist (surrounding whitespace and a
|
|
966
|
+
* trailing `# comment` allowed, case-sensitive)? Pure.
|
|
967
|
+
* @param {string} recipient non-empty
|
|
968
|
+
* @param {string} allowlistText
|
|
969
|
+
*/
|
|
970
|
+
export function hookAllowlistMatch(recipient, allowlistText) {
|
|
971
|
+
const re = new RegExp(`^[ \\t\\v\\f\\r]*${escapeRe(recipient)}[ \\t\\v\\f\\r]*(#[^\\n]*)?$`);
|
|
972
|
+
return String(allowlistText).split("\n").some((line) => re.test(line));
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Phase 1 of the hook verdict: payload shape, banned phrases, openers, persona
|
|
977
|
+
* patterns, internal session names, recipient allowlist. Pure.
|
|
978
|
+
*
|
|
979
|
+
* @param {object} o
|
|
980
|
+
* @param {string} o.channel the hook's channel argument
|
|
981
|
+
* @param {string} o.stdin the raw PreToolUse payload
|
|
982
|
+
* @param {string[]} [o.sessionNames] from hookSessionNames
|
|
983
|
+
* @param {null|{path:string, text:string|null}} [o.allowlist] null = not enforced; text null = unreadable
|
|
984
|
+
* @returns {{allow:true, recipient:string}|{allow:false, reason:string}}
|
|
985
|
+
*/
|
|
986
|
+
export function screenHookPayload({ channel, stdin, sessionNames = [], allowlist = null }) {
|
|
987
|
+
const deny = (reason) => ({ allow: false, reason });
|
|
988
|
+
if (!channel) return deny(HOOK_CHANNEL_REQUIRED);
|
|
989
|
+
const raw = shellCaptured(stdin == null ? "" : stdin);
|
|
990
|
+
if (!raw) return deny("Empty hook payload — nothing to audit (fail closed)");
|
|
991
|
+
let payload;
|
|
992
|
+
// Stricter than `jq -e .`: a stream of several JSON documents is malformed.
|
|
993
|
+
try { payload = JSON.parse(raw); } catch { return deny("Malformed hook payload (not valid JSON) — fail closed"); }
|
|
994
|
+
if (payload === null || payload === false) return deny("Malformed hook payload (not valid JSON) — fail closed");
|
|
995
|
+
const content = extractHookContent(payload);
|
|
996
|
+
if (!content.ok) return deny("Failed to parse outbound content (fail closed)");
|
|
997
|
+
const text = content.text;
|
|
998
|
+
const recipient = extractHookRecipient(payload);
|
|
999
|
+
|
|
1000
|
+
// (a0) Session scaffold — the same shared rule the in-process gate applies,
|
|
1001
|
+
// ahead of the ported rules for the same reason it runs first there. This is
|
|
1002
|
+
// NOT a ported rule: the frozen shell had no counterpart, so it is outside the
|
|
1003
|
+
// parity contract by construction (pre-send-audit.parity.test.mjs says so).
|
|
1004
|
+
const scaffoldReason = screenScaffold(text);
|
|
1005
|
+
if (scaffoldReason) return deny(`Session-scaffold marker in outbound message: ${scaffoldReason}`);
|
|
1006
|
+
|
|
1007
|
+
const lc = text.toLowerCase();
|
|
1008
|
+
for (const phrase of HOOK_BANNED_SUBSTRINGS) {
|
|
1009
|
+
if (lc.includes(phrase.toLowerCase())) {
|
|
1010
|
+
return deny(`Banned AI-tell phrase in outbound message: "${phrase}" (see policies/communication-style.md)`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (HOOK_OPENER_RE.test(text)) {
|
|
1014
|
+
return deny("Banned filler opener (Certainly!/Absolutely!/Great question!) — see policies/communication-style.md");
|
|
1015
|
+
}
|
|
1016
|
+
for (const re of HOOK_PERSONA_PATTERNS) {
|
|
1017
|
+
const m = re.exec(text);
|
|
1018
|
+
if (m) return deny(`Persona leak in outbound message: "${m[0]}" — rewrite in your own voice (my team / a colleague), never name the machinery`);
|
|
1019
|
+
}
|
|
1020
|
+
for (const name of sessionNames) {
|
|
1021
|
+
const hit = hookSessionNameHit(text, name);
|
|
1022
|
+
if (hit) return deny(`Persona leak in outbound message: "${hit}" is an internal session name — say "my team" or "a colleague" instead`);
|
|
1023
|
+
}
|
|
1024
|
+
if (recipient && allowlist) {
|
|
1025
|
+
if (allowlist.text == null || !hookAllowlistMatch(recipient, allowlist.text)) {
|
|
1026
|
+
return deny(`Recipient "${recipient}" not on send allowlist (${allowlist.path})`);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
return { allow: true, recipient };
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/** bash `[ n -ge cap ]` integer syntax; anything else makes the test fail (no cap). */
|
|
1033
|
+
function bashInt(v) {
|
|
1034
|
+
const s = String(v);
|
|
1035
|
+
return /^[ \t\n\v\f\r]*[+-]?[0-9]+[ \t\n\v\f\r]*$/.test(s) ? parseInt(s.trim(), 10) : null;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Phase 2: the hourly + daily send caps over the counter file
|
|
1040
|
+
* (`date<TAB>hour<TAB>hour_count<TAB>day_count`). Pure: `today`/`hour` are the
|
|
1041
|
+
* caller's local-clock strings.
|
|
1042
|
+
*
|
|
1043
|
+
* @param {object} o
|
|
1044
|
+
* @param {string|null} o.counterText counter file contents, null when absent
|
|
1045
|
+
* @param {string} o.today YYYY-MM-DD (local)
|
|
1046
|
+
* @param {string} o.hour HH (local)
|
|
1047
|
+
* @param {string} o.perHourCap raw cap (MAESTRO_SEND_PER_HOUR, default "3000")
|
|
1048
|
+
* @param {string} o.perDayCap raw cap (MAESTRO_SEND_PER_DAY, default "20000")
|
|
1049
|
+
* @returns {{allow:false, reason:string}|{allow:true, counterLine:string, hourCount:number, dayCount:number}}
|
|
1050
|
+
*/
|
|
1051
|
+
export function applyHookRateCaps({ counterText, today, hour, perHourCap, perDayCap }) {
|
|
1052
|
+
let prevDate = "";
|
|
1053
|
+
let prevHour = "";
|
|
1054
|
+
let hourCount = "0";
|
|
1055
|
+
let dayCount = "0";
|
|
1056
|
+
if (counterText != null) {
|
|
1057
|
+
// `IFS=$'\t' read -r a b c d`: first line, runs of tabs are one separator,
|
|
1058
|
+
// the last variable takes the rest of the line.
|
|
1059
|
+
let rest = String(counterText).split("\n")[0].replace(/^\t+/, "").replace(/\t+$/, "");
|
|
1060
|
+
const take = () => {
|
|
1061
|
+
const i = rest.indexOf("\t");
|
|
1062
|
+
if (i < 0) { const v = rest; rest = ""; return v; }
|
|
1063
|
+
const v = rest.slice(0, i);
|
|
1064
|
+
rest = rest.slice(i).replace(/^\t+/, "");
|
|
1065
|
+
return v;
|
|
1066
|
+
};
|
|
1067
|
+
prevDate = take();
|
|
1068
|
+
prevHour = take();
|
|
1069
|
+
hourCount = take();
|
|
1070
|
+
dayCount = rest;
|
|
1071
|
+
}
|
|
1072
|
+
let hc = /^[0-9]+$/.test(hourCount) ? parseInt(hourCount, 10) : 0;
|
|
1073
|
+
let dc = /^[0-9]+$/.test(dayCount) ? parseInt(dayCount, 10) : 0;
|
|
1074
|
+
if (prevDate !== today) { dc = 0; hc = 0; } else if (prevHour !== hour) { hc = 0; }
|
|
1075
|
+
|
|
1076
|
+
const dayCap = bashInt(perDayCap);
|
|
1077
|
+
const hourCap = bashInt(perHourCap);
|
|
1078
|
+
if (dayCap !== null && dc >= dayCap) return { allow: false, reason: `Daily send limit (${perDayCap}) reached. Sends resume tomorrow.` };
|
|
1079
|
+
if (hourCap !== null && hc >= hourCap) return { allow: false, reason: `Hourly send limit (${perHourCap}) reached. Sends resume next hour.` };
|
|
1080
|
+
hc += 1;
|
|
1081
|
+
dc += 1;
|
|
1082
|
+
return { allow: true, counterLine: `${today}\t${hour}\t${hc}\t${dc}\n`, hourCount: hc, dayCount: dc };
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const pad2 = (n) => String(n).padStart(2, "0");
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* The effect edge of the pre-send hook: resolve identity, peers, allowlist and
|
|
1089
|
+
* counter from disk and env, run the two pure phases, commit the counter and
|
|
1090
|
+
* append the send log. Synchronous; never throws for an expected failure —
|
|
1091
|
+
* every denial is `{code:2}` with the reason on stderr, exactly as the shell.
|
|
1092
|
+
*
|
|
1093
|
+
* @param {object} o
|
|
1094
|
+
* @param {string} o.channel
|
|
1095
|
+
* @param {string} o.stdin
|
|
1096
|
+
* @param {object} [o.env=process.env]
|
|
1097
|
+
* @param {Date} [o.now=new Date()]
|
|
1098
|
+
* @param {object} [o.fs] injectable {readFileSync, statSync, mkdirSync, writeFileAtomic, appendJsonl}
|
|
1099
|
+
* @returns {{code:0|2, stdout:string, stderr:string}}
|
|
1100
|
+
*/
|
|
1101
|
+
export function runPreSendHook(o = {}) {
|
|
1102
|
+
const prepared = preparePreSendHook(o);
|
|
1103
|
+
return prepared.ok ? prepared.commit() : prepared.result;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* The pre-send hook in two steps, for a caller that must settle other checks
|
|
1108
|
+
* between the verdict and the send (the Cohort Engine's tool guard): `prepare`
|
|
1109
|
+
* screens the payload and checks the caps WITHOUT writing anything; `commit`
|
|
1110
|
+
* re-reads the counter, re-applies the caps, writes the counter and appends
|
|
1111
|
+
* the send log — exactly what `runPreSendHook` does after the screen. Calling
|
|
1112
|
+
* `commit` only for the send that actually goes out keeps refused calls off
|
|
1113
|
+
* the hourly and daily caps.
|
|
1114
|
+
*
|
|
1115
|
+
* @param {Parameters<typeof runPreSendHook>[0]} o
|
|
1116
|
+
* @returns {{ok:false, result:{code:2, stdout:string, stderr:string}} | {ok:true, commit:() => {code:0|2, stdout:string, stderr:string}}}
|
|
1117
|
+
*/
|
|
1118
|
+
export function preparePreSendHook({ channel, stdin, env = process.env, now = new Date(), fs: injected = {} } = {}) {
|
|
1119
|
+
const fsx = {
|
|
1120
|
+
readFileSync: injected.readFileSync || nodeReadFileSync,
|
|
1121
|
+
statSync: injected.statSync || nodeStatSync,
|
|
1122
|
+
mkdirSync: injected.mkdirSync || nodeMkdirSync,
|
|
1123
|
+
writeFileAtomic: injected.writeFileAtomic || writeFileAtomic,
|
|
1124
|
+
appendJsonl: injected.appendJsonl || appendJsonl,
|
|
1125
|
+
};
|
|
1126
|
+
const deny = (reason) => ({ code: 2, stdout: "", stderr: `BLOCKED: ${reason}\n` });
|
|
1127
|
+
const e = env || {};
|
|
1128
|
+
const isFile = (p) => { try { return fsx.statSync(p).isFile(); } catch { return false; } };
|
|
1129
|
+
const readIfFile = (p) => {
|
|
1130
|
+
if (!isFile(p)) return null;
|
|
1131
|
+
try { return fsx.readFileSync(p, "utf-8"); } catch { return null; /* unreadable: jq/grep would have failed on it */ }
|
|
1132
|
+
};
|
|
1133
|
+
// `${VAR:-}`: an unset or empty variable is the empty string, so the path is
|
|
1134
|
+
// built as the shell built it (e.g. "/config/agent.json" when AGENT_ROOT is unset).
|
|
1135
|
+
const v = (k) => (typeof e[k] === "string" ? e[k] : "");
|
|
1136
|
+
|
|
1137
|
+
const refuse = (reason) => ({ ok: false, result: deny(reason) });
|
|
1138
|
+
if (!channel) return refuse(HOOK_CHANNEL_REQUIRED);
|
|
1139
|
+
|
|
1140
|
+
const first = hookFirstName({
|
|
1141
|
+
envFirst: v("MAESTRO_AGENT_FIRST"),
|
|
1142
|
+
candidates: [`${v("AGENT_ROOT")}/config/agent.json`, `${v("AGENT_DIR")}/config/agent.json`, `${v("HOME")}/.claude/maestro-agent.json`].map(readIfFile),
|
|
1143
|
+
});
|
|
1144
|
+
let peersText = null;
|
|
1145
|
+
for (const p of [`${v("AGENT_ROOT")}/state/session/peers.json`, `${v("AGENT_DIR")}/state/session/peers.json`]) {
|
|
1146
|
+
if (!isFile(p)) continue;
|
|
1147
|
+
peersText = readIfFile(p);
|
|
1148
|
+
break;
|
|
1149
|
+
}
|
|
1150
|
+
const sessionNames = hookSessionNames({ first, peersText, envNames: v("MAESTRO_SESSION_NAMES") });
|
|
1151
|
+
const allowPath = v("MAESTRO_SEND_ALLOWLIST");
|
|
1152
|
+
const allowlist = allowPath && isFile(allowPath) ? { path: allowPath, text: readIfFile(allowPath) } : null;
|
|
1153
|
+
|
|
1154
|
+
const screened = screenHookPayload({ channel, stdin, sessionNames, allowlist });
|
|
1155
|
+
if (!screened.allow) return refuse(screened.reason);
|
|
1156
|
+
|
|
1157
|
+
const runtimeDir = `${v("MAESTRO_RUNTIME_DIR") || `${v("HOME")}/.maestro/runtime`}/audit`;
|
|
1158
|
+
const counterFile = `${runtimeDir}/send-counter.tsv`;
|
|
1159
|
+
const today = `${now.getFullYear()}-${pad2(now.getMonth() + 1)}-${pad2(now.getDate())}`;
|
|
1160
|
+
const hour = pad2(now.getHours());
|
|
1161
|
+
const timestamp = now.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1162
|
+
const perHourCap = v("MAESTRO_SEND_PER_HOUR") || "3000";
|
|
1163
|
+
const perDayCap = v("MAESTRO_SEND_PER_DAY") || "20000";
|
|
1164
|
+
|
|
1165
|
+
// A send the caps already refuse is refused here, before anything is written.
|
|
1166
|
+
const preview = applyHookRateCaps({ counterText: readIfFile(counterFile), today, hour, perHourCap, perDayCap });
|
|
1167
|
+
if (!preview.allow) return refuse(preview.reason);
|
|
1168
|
+
|
|
1169
|
+
return {
|
|
1170
|
+
ok: true,
|
|
1171
|
+
commit() {
|
|
1172
|
+
try {
|
|
1173
|
+
fsx.mkdirSync(runtimeDir, { recursive: true });
|
|
1174
|
+
} catch {
|
|
1175
|
+
return deny(`Cannot create audit runtime dir ${runtimeDir} (fail closed)`);
|
|
1176
|
+
}
|
|
1177
|
+
const caps = applyHookRateCaps({ counterText: readIfFile(counterFile), today, hour, perHourCap, perDayCap });
|
|
1178
|
+
if (!caps.allow) return deny(caps.reason);
|
|
1179
|
+
try {
|
|
1180
|
+
fsx.writeFileAtomic(counterFile, caps.counterLine);
|
|
1181
|
+
} catch {
|
|
1182
|
+
return deny("Cannot write send counter (fail closed)");
|
|
1183
|
+
}
|
|
1184
|
+
// Best effort, as before: the send is already counted.
|
|
1185
|
+
fsx.appendJsonl(`${runtimeDir}/${today}-sends.jsonl`, { timestamp, channel, recipient: screened.recipient, status: "allowed" });
|
|
1186
|
+
|
|
1187
|
+
return {
|
|
1188
|
+
code: 0,
|
|
1189
|
+
stdout: `Audit: ${channel} send screened and logged at ${timestamp} (hour: ${caps.hourCount}/${perHourCap}, day: ${caps.dayCount}/${perDayCap})\n`,
|
|
1190
|
+
stderr: "",
|
|
1191
|
+
};
|
|
1192
|
+
},
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* CLI form of the hook (`--pre-send-hook <channel>`, payload on stdin). Every
|
|
1198
|
+
* outcome is exit 0 or 2: Claude Code does not block on exit 1, so an internal
|
|
1199
|
+
* error here is a denial, never a pass. Output goes through writeSync so a
|
|
1200
|
+
* piped stdout/stderr is flushed before the process ends.
|
|
1201
|
+
*/
|
|
1202
|
+
function mainPreSendHook(argv) {
|
|
1203
|
+
let res;
|
|
1204
|
+
try {
|
|
1205
|
+
const channel = argv[1] || "";
|
|
1206
|
+
res = channel
|
|
1207
|
+
? runPreSendHook({ channel, stdin: readStdin(), env: process.env, now: new Date() })
|
|
1208
|
+
: { code: 2, stdout: "", stderr: `BLOCKED: ${HOOK_CHANNEL_REQUIRED}\n` };
|
|
1209
|
+
} catch (err) {
|
|
1210
|
+
res = { code: 2, stdout: "", stderr: `BLOCKED: pre-send gate internal error — ${err && err.message ? err.message : String(err)} (fail closed)\n` };
|
|
1211
|
+
}
|
|
1212
|
+
try {
|
|
1213
|
+
if (res.stdout) nodeWriteSync(1, res.stdout);
|
|
1214
|
+
if (res.stderr) nodeWriteSync(2, res.stderr);
|
|
1215
|
+
} catch { /* an unwritable stdio cannot change the verdict carried by the exit code */ }
|
|
1216
|
+
process.exitCode = res.code;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
663
1219
|
/* ─────────────────────────────────── CLI ─────────────────────────────────── */
|
|
664
1220
|
|
|
665
1221
|
/**
|
|
@@ -742,5 +1298,7 @@ async function main(argv) {
|
|
|
742
1298
|
const invokedDirectly =
|
|
743
1299
|
process.argv[1] && process.argv[1].endsWith("send-gate.mjs");
|
|
744
1300
|
if (invokedDirectly) {
|
|
745
|
-
|
|
1301
|
+
const cliArgs = process.argv.slice(2);
|
|
1302
|
+
if (cliArgs[0] === "--pre-send-hook") mainPreSendHook(cliArgs);
|
|
1303
|
+
else main(cliArgs);
|
|
746
1304
|
}
|