@cohortapp/agent-sdk 2.17.0 → 2.18.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +18 -0
- package/.env.example +18 -5
- package/README.md +1 -0
- package/bin/maestro.mjs +62 -0
- package/docs/guides/billing-console-keys.md +60 -0
- package/docs/guides/front-door-session.md +54 -9
- package/docs/guides/mac-mini.md +20 -25
- package/docs/guides/setup-wizard.md +1 -1
- package/docs/runbooks/fleet-rollout.md +156 -0
- package/docs/runbooks/mac-mini-bootstrap.md +12 -14
- package/lib/action-executor.js +19 -3
- package/lib/budget-guard.mjs +279 -3
- package/lib/channels/base-adapter.mjs +3 -1
- package/lib/channels/contract.mjs +2 -1
- package/lib/channels/inbox-item.mjs +8 -0
- package/lib/claude-bin.mjs +5 -6
- package/lib/cli/doctor-checks.mjs +141 -10
- package/lib/cli/global-setup-extras.mjs +5 -1
- package/lib/cli/inbox.mjs +100 -15
- package/lib/cli/seat-auth.mjs +463 -0
- package/lib/cli/session.mjs +80 -12
- package/lib/collective/capture-slots.mjs +234 -0
- package/lib/collective/capture.mjs +8 -6
- package/lib/collective/config.mjs +2 -0
- package/lib/collective/global-config.mjs +63 -1
- package/lib/collective/loop-guard.mjs +155 -0
- package/lib/collective/presence.mjs +142 -5
- package/lib/comms/send-gate.mjs +559 -1
- package/lib/diagnostics/alerts.mjs +49 -0
- package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
- package/lib/engine/agents/definitions.mjs +343 -0
- package/lib/engine/agents/persist.mjs +275 -0
- package/lib/engine/agents/runtime.mjs +748 -0
- package/lib/engine/agents/usage.mjs +95 -0
- package/lib/engine/auth-status.mjs +139 -0
- package/lib/engine/budget.mjs +194 -0
- package/lib/engine/cli.mjs +1204 -0
- package/lib/engine/commands/index.mjs +269 -0
- package/lib/engine/context/budget.mjs +219 -0
- package/lib/engine/context/cache.mjs +125 -0
- package/lib/engine/context/child-env.mjs +215 -0
- package/lib/engine/context/compaction.mjs +342 -0
- package/lib/engine/context/images.mjs +90 -0
- package/lib/engine/context/instructions.mjs +327 -0
- package/lib/engine/context/lazy-instructions.mjs +169 -0
- package/lib/engine/context/manager.mjs +182 -0
- package/lib/engine/context/real-path.mjs +91 -0
- package/lib/engine/context/secret-values.mjs +163 -0
- package/lib/engine/context/settings.mjs +274 -0
- package/lib/engine/context/stream-input.mjs +159 -0
- package/lib/engine/guard.mjs +152 -0
- package/lib/engine/hooks.mjs +713 -0
- package/lib/engine/loop.mjs +560 -0
- package/lib/engine/mcp/client.mjs +254 -0
- package/lib/engine/mcp/config.mjs +301 -0
- package/lib/engine/mcp/http.mjs +201 -0
- package/lib/engine/mcp/index.mjs +146 -0
- package/lib/engine/mcp/jsonrpc.mjs +147 -0
- package/lib/engine/mcp/naming.mjs +66 -0
- package/lib/engine/mcp/resources.mjs +89 -0
- package/lib/engine/mcp/results.mjs +133 -0
- package/lib/engine/mcp/stdio.mjs +137 -0
- package/lib/engine/mcp/supervisor.mjs +116 -0
- package/lib/engine/messages.mjs +104 -0
- package/lib/engine/output/json.mjs +164 -0
- package/lib/engine/output/stream-json.mjs +266 -0
- package/lib/engine/permissions.mjs +845 -0
- package/lib/engine/process-identity.mjs +164 -0
- package/lib/engine/process-tree.mjs +551 -0
- package/lib/engine/prompt.mjs +60 -0
- package/lib/engine/session/store.mjs +299 -0
- package/lib/engine/session-runtime/args.mjs +97 -0
- package/lib/engine/session-runtime/host.mjs +143 -0
- package/lib/engine/session-runtime/inbox.mjs +122 -0
- package/lib/engine/session-runtime/notifications.mjs +129 -0
- package/lib/engine/session-runtime/registry.mjs +328 -0
- package/lib/engine/session-runtime/runner.mjs +344 -0
- package/lib/engine/session-runtime/socket.mjs +212 -0
- package/lib/engine/session-runtime/wakeup.mjs +115 -0
- package/lib/engine/skills/index.mjs +321 -0
- package/lib/engine/tools/bash-background.mjs +533 -0
- package/lib/engine/tools/bash.mjs +216 -0
- package/lib/engine/tools/edit.mjs +97 -0
- package/lib/engine/tools/glob.mjs +81 -0
- package/lib/engine/tools/grep.mjs +224 -0
- package/lib/engine/tools/index.mjs +84 -0
- package/lib/engine/tools/list-agents.mjs +32 -0
- package/lib/engine/tools/ls.mjs +127 -0
- package/lib/engine/tools/monitor.mjs +82 -0
- package/lib/engine/tools/notebook-edit.mjs +218 -0
- package/lib/engine/tools/read.mjs +103 -0
- package/lib/engine/tools/schedule-wakeup.mjs +45 -0
- package/lib/engine/tools/schema.mjs +144 -0
- package/lib/engine/tools/send-message.mjs +77 -0
- package/lib/engine/tools/session.mjs +70 -0
- package/lib/engine/tools/todo.mjs +144 -0
- package/lib/engine/tools/toolsearch.mjs +217 -0
- package/lib/engine/tools/walk.mjs +193 -0
- package/lib/engine/tools/web-switch.mjs +31 -0
- package/lib/engine/tools/webfetch-html.mjs +387 -0
- package/lib/engine/tools/webfetch-net.mjs +340 -0
- package/lib/engine/tools/webfetch.mjs +198 -0
- package/lib/engine/tools/websearch.mjs +91 -0
- package/lib/engine/tools/workflow.mjs +95 -0
- package/lib/engine/tools/write.mjs +76 -0
- package/lib/engine/tui/line-editor.mjs +137 -0
- package/lib/engine/tui/render.mjs +86 -0
- package/lib/engine/tui/tui.mjs +274 -0
- package/lib/engine/wire/anthropic-messages.mjs +263 -0
- package/lib/engine/wire/effort.mjs +36 -0
- package/lib/engine/wire/errors.mjs +496 -0
- package/lib/engine/wire/http.mjs +441 -0
- package/lib/engine/wire/index.mjs +76 -0
- package/lib/engine/wire/openai-chat.mjs +332 -0
- package/lib/engine/wire/prompt-cache.mjs +79 -0
- package/lib/engine/wire/search.mjs +140 -0
- package/lib/engine/wire/sse.mjs +114 -0
- package/lib/engine/wire/stall.mjs +349 -0
- package/lib/engine/wire/token-provider.mjs +175 -0
- package/lib/engine/wire/usage.mjs +192 -0
- package/lib/engine/workflow/host.mjs +524 -0
- package/lib/engine/workflow/journal.mjs +188 -0
- package/lib/engine/workflow/json-schema.mjs +171 -0
- package/lib/engine/workflow/meta.mjs +329 -0
- package/lib/engine/workflow/notifications.mjs +52 -0
- package/lib/engine/workflow/runtime.mjs +447 -0
- package/lib/engine/workflow/sandbox.mjs +534 -0
- package/lib/engine/workflow/worker.mjs +141 -0
- package/lib/engine/workflow/worktree.mjs +74 -0
- package/lib/execution/disposition.mjs +1 -1
- package/lib/execution/intake.mjs +10 -0
- package/lib/execution/surface-policy.mjs +15 -0
- package/lib/learning/curator.mjs +8 -6
- package/lib/learning/reflect.mjs +8 -6
- package/lib/model-router/catalog/cohort.yaml +137 -0
- package/lib/model-router/catalog.mjs +118 -1
- package/lib/model-router/failover.mjs +67 -16
- package/lib/model-router/llm-task.mjs +39 -3
- package/lib/model-router/resolve.mjs +89 -3
- package/lib/model-router/spawn.mjs +46 -47
- package/lib/model-router/taxonomy.mjs +126 -4
- package/lib/org/cost-sync.mjs +141 -11
- package/lib/org/inbound/broadcast.mjs +289 -0
- package/lib/org/inbound/collective.mjs +375 -0
- package/lib/org/inbound/directedness.mjs +96 -8
- package/lib/org/inbound/facts.mjs +78 -2
- package/lib/org/inbound/project.mjs +22 -0
- package/lib/org/inbound/surfaces.mjs +14 -0
- package/lib/org/llm-token.mjs +879 -0
- package/lib/org/mesh.mjs +61 -0
- package/lib/org/messaging.mjs +3 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +15 -0
- package/lib/org/quota.mjs +520 -0
- package/lib/org/tool-surface.mjs +104 -16
- package/lib/org/ui-parity.mjs +16 -1
- package/lib/org/work-ledger.mjs +37 -6
- package/lib/rate-guard.mjs +114 -1
- package/lib/resource-governor.mjs +41 -6
- package/lib/runtime/adapter.mjs +833 -0
- package/lib/runtime/child-env.mjs +191 -0
- package/lib/runtime/legacy-shell-guard.mjs +97 -0
- package/lib/runtime/seat-engine.mjs +162 -0
- package/lib/session/ask-ledger.mjs +271 -0
- package/lib/session/current-work.mjs +676 -0
- package/lib/session/feed-core.mjs +40 -3
- package/lib/session/launch-args.mjs +56 -4
- package/lib/session/status-summary.mjs +26 -9
- package/lib/session/upgrade-notice.mjs +42 -0
- package/lib/setup/claude-probe.mjs +117 -13
- package/lib/setup/enrich.mjs +13 -10
- package/lib/setup/sections/model.mjs +39 -13
- package/lib/telemetry/collect.mjs +229 -11
- package/lib/upgrade/ignored-drift.mjs +105 -0
- package/lib/voice/post-call-brief.mjs +30 -17
- package/package.json +13 -3
- package/plugins/maestro-skills/skills/board-work.md +5 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
- package/plugins/maestro-skills/skills/main-session.md +18 -7
- package/scaffold/config/collective.yaml +7 -0
- package/scripts/ci/check-durable-write-seam.mjs +3 -1
- package/scripts/ci/check-tarball-fidelity.mjs +126 -2
- package/scripts/ci/run-tests.mjs +47 -19
- package/scripts/cohort-llm/api-key-helper.mjs +92 -0
- package/scripts/collective/hook-runner.mjs +142 -19
- package/scripts/continuous-monitor.sh +13 -0
- package/scripts/cost/track-claude-usage.mjs +15 -0
- package/scripts/daemon/agent-daemon.mjs +408 -20
- package/scripts/daemon/assurance.mjs +48 -12
- package/scripts/daemon/cadence-consumer.mjs +218 -68
- package/scripts/daemon/cadence-handlers.mjs +73 -4
- package/scripts/daemon/classifier.mjs +75 -26
- package/scripts/daemon/context-compiler.mjs +51 -37
- package/scripts/daemon/deliver.mjs +30 -1
- package/scripts/daemon/dispatcher.mjs +595 -149
- package/scripts/daemon/health.mjs +14 -1
- package/scripts/daemon/maestro-daemon.mjs +11 -0
- package/scripts/daemon/prompt-builder.mjs +24 -0
- package/scripts/daemon/responder.mjs +246 -79
- package/scripts/daemon/sdk-version.mjs +98 -16
- package/scripts/eval/probe-gateway.mjs +635 -0
- package/scripts/eval/replay/extract.mjs +270 -0
- package/scripts/eval/replay/grade.mjs +260 -0
- package/scripts/eval/replay/lib/config.mjs +50 -0
- package/scripts/eval/replay/lib/effects.mjs +65 -0
- package/scripts/eval/replay/lib/fixture.mjs +188 -0
- package/scripts/eval/replay/lib/judge.mjs +72 -0
- package/scripts/eval/replay/lib/redact.mjs +136 -0
- package/scripts/eval/replay/lib/sandbox.mjs +170 -0
- package/scripts/eval/replay/lib/schema-check.mjs +63 -0
- package/scripts/eval/replay/lib/transcript.mjs +76 -0
- package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
- package/scripts/eval/replay/report.mjs +185 -0
- package/scripts/eval/replay/run.mjs +404 -0
- package/scripts/fleet/rollout.mjs +1151 -0
- package/scripts/hooks/pre-send-audit.sh +36 -245
- package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
- package/scripts/hooks/validate-state-yaml.sh +190 -0
- package/scripts/huddle/huddle-llm.mjs +361 -0
- package/scripts/huddle/huddle-server.mjs +46 -121
- package/scripts/local-triggers/autoupdate.sh +465 -81
- package/scripts/local-triggers/run-trigger.sh +13 -0
- package/scripts/maintenance/pin-integrity.mjs +364 -0
- package/scripts/poll-slack-events.sh +41 -9
- package/scripts/poller/slack-socket-mode.mjs +28 -3
- package/scripts/session/supervisor.mjs +80 -13
- package/scripts/spawn-session.sh +13 -0
- package/bin/maestro.test.mjs +0 -1574
- package/lib/action-executor.test.mjs +0 -871
- package/lib/archetype.test.mjs +0 -132
- package/lib/assurance/plan-note.test.mjs +0 -234
- package/lib/assurance/room-budget.test.mjs +0 -486
- package/lib/assurance/tier.test.mjs +0 -174
- package/lib/autonomy.test.mjs +0 -66
- package/lib/backlog.test.mjs +0 -302
- package/lib/backup/policy.test.mjs +0 -305
- package/lib/budget-escalate.test.mjs +0 -232
- package/lib/budget-guard.envelope.test.mjs +0 -476
- package/lib/budget-guard.test.mjs +0 -427
- package/lib/cadence-bus-requeue.test.mjs +0 -83
- package/lib/cadence-bus-schedule.test.mjs +0 -194
- package/lib/cadence-bus.test.mjs +0 -720
- package/lib/cadences.test.mjs +0 -230
- package/lib/capability/inventory.test.mjs +0 -232
- package/lib/capability.test.mjs +0 -78
- package/lib/channels/base-adapter.test.mjs +0 -590
- package/lib/channels/channels.test.mjs +0 -371
- package/lib/channels/contract.test.mjs +0 -162
- package/lib/channels/inbox-item.test.mjs +0 -368
- package/lib/channels/orgmail/adapter.test.mjs +0 -448
- package/lib/channels/pairing.test.mjs +0 -270
- package/lib/channels/repeat-suppressor.test.mjs +0 -134
- package/lib/channels/slack-adapter.test.mjs +0 -212
- package/lib/channels/telegram-adapter.test.mjs +0 -306
- package/lib/channels/voice/adapter.test.mjs +0 -278
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
- package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
- package/lib/charter.test.mjs +0 -89
- package/lib/claude-bin.test.mjs +0 -131
- package/lib/cli/board.test.mjs +0 -227
- package/lib/cli/design.test.mjs +0 -270
- package/lib/cli/doctor-checks.test.mjs +0 -336
- package/lib/cli/global-setup-extras.test.mjs +0 -462
- package/lib/cli/inbox.test.mjs +0 -230
- package/lib/cli/session-ack.test.mjs +0 -63
- package/lib/cli/session.test.mjs +0 -613
- package/lib/collective/capture.test.mjs +0 -121
- package/lib/collective/cards.test.mjs +0 -114
- package/lib/collective/config.test.mjs +0 -123
- package/lib/collective/global-config.test.mjs +0 -220
- package/lib/collective/global-skills.test.mjs +0 -126
- package/lib/collective/presence.test.mjs +0 -95
- package/lib/collective/recall.test.mjs +0 -116
- package/lib/collective/vendor-skills.test.mjs +0 -306
- package/lib/comms/send-gate.test.mjs +0 -770
- package/lib/comms.test.mjs +0 -41
- package/lib/context/budget.test.mjs +0 -252
- package/lib/context/history-scope.test.mjs +0 -79
- package/lib/cost/ledger-row.test.mjs +0 -183
- package/lib/design/design-md.test.mjs +0 -318
- package/lib/design/fixtures/DESIGN.golden.md +0 -238
- package/lib/design/fixtures/PRODUCT.golden.md +0 -67
- package/lib/design/fixtures/foundation.json +0 -133
- package/lib/design/refresh-gate.test.mjs +0 -144
- package/lib/design/write.test.mjs +0 -241
- package/lib/diagnostics/alerts.test.mjs +0 -318
- package/lib/diagnostics/backup-freshness.test.mjs +0 -185
- package/lib/diagnostics/counters.test.mjs +0 -206
- package/lib/diagnostics/events.test.mjs +0 -290
- package/lib/diagnostics/otel.test.mjs +0 -196
- package/lib/diagnostics/trace.test.mjs +0 -251
- package/lib/env-compat.test.mjs +0 -104
- package/lib/execution/disposition.test.mjs +0 -553
- package/lib/execution/drive.test.mjs +0 -270
- package/lib/execution/effects.test.mjs +0 -344
- package/lib/execution/intake.test.mjs +0 -389
- package/lib/execution/journal.test.mjs +0 -261
- package/lib/execution/match.test.mjs +0 -235
- package/lib/execution/pipeline.test.mjs +0 -392
- package/lib/execution/route.test.mjs +0 -186
- package/lib/execution/surface-policy.test.mjs +0 -162
- package/lib/fs-atomic.test.mjs +0 -72
- package/lib/fs-ownership.test.mjs +0 -158
- package/lib/goals/admission.test.mjs +0 -164
- package/lib/goals/classify.test.mjs +0 -167
- package/lib/goals/collaborate.test.mjs +0 -336
- package/lib/goals/gaps.test.mjs +0 -284
- package/lib/goals/loop.test.mjs +0 -845
- package/lib/hooks/bus.test.mjs +0 -387
- package/lib/identity/persona.test.mjs +0 -142
- package/lib/kpi-sensors.test.mjs +0 -278
- package/lib/kpi.test.mjs +0 -244
- package/lib/learning/config.test.mjs +0 -75
- package/lib/learning/counters.test.mjs +0 -69
- package/lib/learning/curator-consolidate.test.mjs +0 -238
- package/lib/learning/curator.test.mjs +0 -106
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.test.mjs +0 -125
- package/lib/learning/skill-writer.test.mjs +0 -210
- package/lib/mandate/audit.test.mjs +0 -195
- package/lib/mandate/contract.test.mjs +0 -185
- package/lib/mandate/derive.test.mjs +0 -274
- package/lib/mandate/model.test.mjs +0 -164
- package/lib/mandate/refresh.test.mjs +0 -389
- package/lib/mcp/server.test.mjs +0 -426
- package/lib/model-router/auth-profiles.test.mjs +0 -580
- package/lib/model-router/catalog.test.mjs +0 -385
- package/lib/model-router/economics.test.mjs +0 -438
- package/lib/model-router/failover.test.mjs +0 -439
- package/lib/model-router/health.test.mjs +0 -338
- package/lib/model-router/integration-coverage.test.mjs +0 -831
- package/lib/model-router/integration.test.mjs +0 -564
- package/lib/model-router/ledger.test.mjs +0 -415
- package/lib/model-router/llm-task.test.mjs +0 -392
- package/lib/model-router/org-credentials.test.mjs +0 -265
- package/lib/model-router/pricing-refresh.test.mjs +0 -286
- package/lib/model-router/reconcile.test.mjs +0 -316
- package/lib/model-router/repair.test.mjs +0 -180
- package/lib/model-router/spawn.test.mjs +0 -446
- package/lib/model-router/taxonomy.test.mjs +0 -410
- package/lib/model-router.test.mjs +0 -1207
- package/lib/org/activity.test.mjs +0 -134
- package/lib/org/approvals.test.mjs +0 -216
- package/lib/org/awareness.test.mjs +0 -159
- package/lib/org/board-mine-cache.test.mjs +0 -53
- package/lib/org/board.test.mjs +0 -187
- package/lib/org/bootstrap-context.test.mjs +0 -153
- package/lib/org/client.test.mjs +0 -1206
- package/lib/org/cohort-client.test.mjs +0 -126
- package/lib/org/cost-sync.test.mjs +0 -153
- package/lib/org/doctor.test.mjs +0 -346
- package/lib/org/engagement-ledger.test.mjs +0 -112
- package/lib/org/engagement.test.mjs +0 -739
- package/lib/org/handoff.test.mjs +0 -269
- package/lib/org/inbound/directedness.test.mjs +0 -668
- package/lib/org/inbound/facts.test.mjs +0 -471
- package/lib/org/inbound/hydrate.test.mjs +0 -908
- package/lib/org/inbound/index.test.mjs +0 -429
- package/lib/org/inbound/project.test.mjs +0 -287
- package/lib/org/integration-tools.test.mjs +0 -160
- package/lib/org/keys.test.mjs +0 -92
- package/lib/org/knowledge.test.mjs +0 -326
- package/lib/org/leases.test.mjs +0 -235
- package/lib/org/mesh-directives.test.mjs +0 -110
- package/lib/org/mesh-integration.test.mjs +0 -127
- package/lib/org/mesh.test.mjs +0 -400
- package/lib/org/messaging.test.mjs +0 -471
- package/lib/org/param-contract.test.mjs +0 -477
- package/lib/org/policy.test.mjs +0 -237
- package/lib/org/protocol.checksum.test.mjs +0 -90
- package/lib/org/protocol.test.mjs +0 -323
- package/lib/org/push.test.mjs +0 -792
- package/lib/org/registry.test.mjs +0 -100
- package/lib/org/resource-tools.test.mjs +0 -361
- package/lib/org/tool-access.test.mjs +0 -144
- package/lib/org/tool-surface-integration.test.mjs +0 -120
- package/lib/org/tool-surface.test.mjs +0 -1268
- package/lib/org/typing.test.mjs +0 -291
- package/lib/org/ui-parity.test.mjs +0 -560
- package/lib/org/verify.test.mjs +0 -194
- package/lib/org/work-ledger.test.mjs +0 -273
- package/lib/plan/adoption-e2e.test.mjs +0 -366
- package/lib/plan/budget-enforcement.test.mjs +0 -400
- package/lib/plan/compile.test.mjs +0 -382
- package/lib/plan/emit.test.mjs +0 -269
- package/lib/plan/explain.test.mjs +0 -188
- package/lib/prompts/parallelism.test.mjs +0 -177
- package/lib/rag/rag.test.mjs +0 -505
- package/lib/rate-guard.test.mjs +0 -272
- package/lib/reactive-gate.test.mjs +0 -57
- package/lib/render.test.mjs +0 -68
- package/lib/resource-governor.test.mjs +0 -488
- package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
- package/lib/scheduling/jitter.test.mjs +0 -140
- package/lib/secrets/broker.test.mjs +0 -280
- package/lib/secrets/providers.test.mjs +0 -274
- package/lib/security/audit-engine.test.mjs +0 -424
- package/lib/security/coerce-args.test.mjs +0 -281
- package/lib/security/dangerous-tools.test.mjs +0 -68
- package/lib/security/external-content.test.mjs +0 -84
- package/lib/security/redact.test.mjs +0 -441
- package/lib/security/secret-equal.test.mjs +0 -55
- package/lib/session/config.test.mjs +0 -92
- package/lib/session/feed-core.test.mjs +0 -198
- package/lib/session/first-run.test.mjs +0 -121
- package/lib/session/frontdoor.test.mjs +0 -205
- package/lib/session/handoffs.test.mjs +0 -183
- package/lib/session/identity.test.mjs +0 -180
- package/lib/session/inbox-claims.test.mjs +0 -286
- package/lib/session/launch-args.test.mjs +0 -157
- package/lib/session/liveness.test.mjs +0 -100
- package/lib/session/status-summary.test.mjs +0 -118
- package/lib/session-permissions.test.mjs +0 -120
- package/lib/setup/claude-probe.test.mjs +0 -187
- package/lib/setup/completeness.test.mjs +0 -110
- package/lib/setup/context-pack.test.mjs +0 -89
- package/lib/setup/enrich.test.mjs +0 -115
- package/lib/setup/enroll-from-cohort.test.mjs +0 -300
- package/lib/setup/integration.test.mjs +0 -162
- package/lib/setup/io.test.mjs +0 -77
- package/lib/setup/runner.test.mjs +0 -132
- package/lib/setup/sections/identity.test.mjs +0 -234
- package/lib/setup/sections/inventory.test.mjs +0 -198
- package/lib/setup/sections/learning.test.mjs +0 -81
- package/lib/setup/sections/mandate.test.mjs +0 -388
- package/lib/setup/sections/messaging.test.mjs +0 -127
- package/lib/setup/sections/model.test.mjs +0 -240
- package/lib/setup/sections/org.test.mjs +0 -346
- package/lib/setup/sections/orgmail.test.mjs +0 -118
- package/lib/setup/sections/recovery.test.mjs +0 -98
- package/lib/setup/sections/subagents.test.mjs +0 -429
- package/lib/setup/sections/verify.test.mjs +0 -175
- package/lib/setup/sot.test.mjs +0 -81
- package/lib/setup/state.test.mjs +0 -115
- package/lib/singleton.test.mjs +0 -151
- package/lib/subagents/cli.test.mjs +0 -389
- package/lib/subagents/client.test.mjs +0 -309
- package/lib/subagents/gap.test.mjs +0 -234
- package/lib/subagents/lock.test.mjs +0 -248
- package/lib/subagents/manifest.test.mjs +0 -175
- package/lib/subagents/refs.test.mjs +0 -204
- package/lib/subagents/resolve.test.mjs +0 -422
- package/lib/subagents/schema.test.mjs +0 -328
- package/lib/telemetry/alerts.test.mjs +0 -109
- package/lib/telemetry/collect.test.mjs +0 -1274
- package/lib/tool-definitions-integration.test.mjs +0 -83
- package/lib/tool-definitions.test.mjs +0 -437
- package/lib/upgrade/global-refresh.test.mjs +0 -65
- package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
- package/lib/upgrade/post-steps.test.mjs +0 -200
- package/lib/upgrade/verify.test.mjs +0 -164
- package/lib/util/fetch-timeout.test.mjs +0 -202
- package/lib/util/reconnect.test.mjs +0 -369
- package/lib/util/unhandled.test.mjs +0 -216
- package/lib/voice/outbound.test.mjs +0 -69
- package/lib/voice/session-rotation.test.mjs +0 -114
- package/lib/voice/stt.test.mjs +0 -226
- package/lib/voice/voice.test.mjs +0 -990
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
- package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
- package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
- package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
- package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
- package/scripts/ci/check-skill-packs.test.mjs +0 -495
- package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
- package/scripts/ci/check.test.mjs +0 -194
- package/scripts/ci/conformance-org-api.test.mjs +0 -425
- package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
- package/scripts/collective/hook-runner.test.mjs +0 -173
- package/scripts/cost/fleet-digest.test.mjs +0 -207
- package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
- package/scripts/cost/track-claude-usage.test.mjs +0 -148
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
- package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
- package/scripts/daemon/agent-daemon.test.mjs +0 -995
- package/scripts/daemon/assurance-e2e.test.mjs +0 -613
- package/scripts/daemon/assurance.test.mjs +0 -1791
- package/scripts/daemon/board-mirror.test.mjs +0 -165
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
- package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
- package/scripts/daemon/cadence-consumer.test.mjs +0 -776
- package/scripts/daemon/cadence-handlers.test.mjs +0 -837
- package/scripts/daemon/classifier-identity.test.mjs +0 -137
- package/scripts/daemon/classifier.test.mjs +0 -266
- package/scripts/daemon/classify-kind.test.mjs +0 -40
- package/scripts/daemon/context-compiler.test.mjs +0 -406
- package/scripts/daemon/deliver.test.mjs +0 -564
- package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
- package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
- package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
- package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
- package/scripts/daemon/execution-ladder.test.mjs +0 -470
- package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
- package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
- package/scripts/daemon/inbox-deferral.test.mjs +0 -336
- package/scripts/daemon/inbox-wake.test.mjs +0 -199
- package/scripts/daemon/integration.test.mjs +0 -149
- package/scripts/daemon/lib/self-echo.test.mjs +0 -153
- package/scripts/daemon/lib/session-router.test.mjs +0 -554
- package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
- package/scripts/daemon/prompt-builder.test.mjs +0 -556
- package/scripts/daemon/responder-cost.test.mjs +0 -68
- package/scripts/daemon/responder-history.test.mjs +0 -221
- package/scripts/daemon/sdk-version.test.mjs +0 -31
- package/scripts/daemon/session-lock.test.mjs +0 -252
- package/scripts/daemon/session-outcomes.test.mjs +0 -533
- package/scripts/daemon/typing-registry.test.mjs +0 -102
- package/scripts/hooks/pre-send-audit.test.mjs +0 -354
- package/scripts/huddle/huddle-prompt.test.mjs +0 -176
- package/scripts/local-triggers/autoupdate.test.mjs +0 -518
- package/scripts/local-triggers/generate-plists.test.mjs +0 -456
- package/scripts/media-generation/brand-clause.test.mjs +0 -135
- package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
- package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
- package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
- package/scripts/poller/slack-socket-mode.test.mjs +0 -805
- package/scripts/poller-launchd/install.test.mjs +0 -243
- package/scripts/restore-from-backup.test.mjs +0 -181
- package/scripts/session/feed.test.mjs +0 -196
- package/scripts/session/supervisor-sh.test.mjs +0 -218
- package/scripts/session/supervisor.test.mjs +0 -482
- package/scripts/setup/configure-macos.test.mjs +0 -306
- package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
- package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
- package/scripts/setup/generate-capability.test.mjs +0 -134
- package/scripts/setup/init-agent.test.mjs +0 -370
- package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
- package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
- package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
|
@@ -1,267 +1,58 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# Pre-send audit hook — gates outbound communications
|
|
3
|
-
# Called by Claude Code PreToolUse hook before Slack/Gmail/WhatsApp sends.
|
|
4
|
-
# Reads the PreToolUse payload from stdin, screens the outbound CONTENT
|
|
5
|
-
# against the banned-phrase list (policies/communication-style.md), runs a
|
|
6
|
-
# recipient allowlist sanity check, and enforces hourly + daily rate caps.
|
|
2
|
+
# Pre-send audit hook — gates outbound communications.
|
|
3
|
+
# Called by Claude Code PreToolUse hook before Slack/Gmail/WhatsApp/Cohort sends.
|
|
7
4
|
#
|
|
8
5
|
# Usage: pre-send-audit.sh <channel_type>
|
|
9
6
|
# channel_type: slack | slack-draft | gmail | whatsapp | cohort
|
|
10
7
|
#
|
|
11
8
|
# `cohort` is the PreToolUse channel `maestro global-setup` wires into
|
|
12
9
|
# ~/.claude/settings.json for EVERY mcp__cohort outbound tool (messaging_send,
|
|
13
|
-
# messaging_send_voice_note, email_send, email_draft_send, org_rpc)
|
|
14
|
-
# persona gate below (section a2) is what the front-door session relies on to
|
|
15
|
-
# never expose its own machinery to a human (design spec 2026-09-08 §3.6). The
|
|
16
|
-
# same patterns run in-process in lib/comms/send-gate.mjs (screenPersona) for
|
|
17
|
-
# the lanes no hook fronts: `maestro inbox reply`, the daemon's delivery, the
|
|
18
|
-
# adapters. Keep the two lists in sync.
|
|
10
|
+
# messaging_send_voice_note, email_send, email_draft_send, org_rpc).
|
|
19
11
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
12
|
+
# THIN WRAPPER. Every rule this script used to carry in bash + jq — the banned
|
|
13
|
+
# AI-tell phrases (policies/communication-style.md), the filler openers, the
|
|
14
|
+
# persona gate (PERSONA_PATTERNS and internal session names: <first>-main,
|
|
15
|
+
# state/session/peers.json, MAESTRO_SESSION_NAMES), the recipient allowlist
|
|
16
|
+
# (MAESTRO_SEND_ALLOWLIST) and the hourly/daily caps (MAESTRO_SEND_PER_HOUR /
|
|
17
|
+
# MAESTRO_SEND_PER_DAY, counters under MAESTRO_RUNTIME_DIR) — now runs in
|
|
18
|
+
# lib/comms/send-gate.mjs (`--pre-send-hook`), the module every in-process send
|
|
19
|
+
# path already shares. One implementation means the Claude Code hook and a
|
|
20
|
+
# native engine gate cannot drift. scripts/hooks/pre-send-audit.parity.test.mjs
|
|
21
|
+
# proves the verdicts, reasons and counters match the previous shell script
|
|
22
|
+
# (kept as scripts/hooks/fixtures/pre-send-audit.legacy.sh) over the hook's
|
|
23
|
+
# test corpus.
|
|
24
24
|
#
|
|
25
|
-
#
|
|
26
|
-
# 0 = allowed (stdout message shown to Claude)
|
|
27
|
-
# 2 = blocked / any parse|IO error (stderr reason shown as rejection).
|
|
28
|
-
# Claude Code blocks the tool call on any non-zero exit, so EVERY
|
|
29
|
-
# failure path below intentionally fails CLOSED with exit 2.
|
|
25
|
+
# Stdin: the Claude Code PreToolUse JSON payload, passed through untouched.
|
|
30
26
|
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
27
|
+
# Exit codes:
|
|
28
|
+
# 0 = allowed (stdout shown to Claude)
|
|
29
|
+
# 2 = blocked (stderr reason shown as the rejection)
|
|
30
|
+
# Claude Code does NOT block on exit 1, so every failure here — no node, no
|
|
31
|
+
# gate module, a crash inside the gate — is mapped to exit 2 (fail closed).
|
|
35
32
|
|
|
36
33
|
set -uo pipefail
|
|
37
34
|
|
|
38
|
-
# --- fail-closed helper -----------------------------------------------------
|
|
39
35
|
deny() {
|
|
40
|
-
# $1 = human-readable reason
|
|
41
36
|
echo "BLOCKED: ${1:-pre-send audit failed}" >&2
|
|
42
37
|
exit 2
|
|
43
38
|
}
|
|
44
39
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# jq is mandatory — without it we cannot inspect content, so refuse to send.
|
|
49
|
-
command -v jq >/dev/null 2>&1 || deny "jq not available — cannot screen outbound content (fail closed)"
|
|
50
|
-
|
|
51
|
-
# --- read stdin payload -----------------------------------------------------
|
|
52
|
-
TOOL_INPUT=$(cat)
|
|
53
|
-
[ -n "$TOOL_INPUT" ] || deny "Empty hook payload — nothing to audit (fail closed)"
|
|
54
|
-
|
|
55
|
-
# Validate that the payload is well-formed JSON before doing anything else.
|
|
56
|
-
if ! printf '%s' "$TOOL_INPUT" | jq -e . >/dev/null 2>&1; then
|
|
57
|
-
deny "Malformed hook payload (not valid JSON) — fail closed"
|
|
58
|
-
fi
|
|
59
|
-
|
|
60
|
-
# Extract the outbound text. Claude Code wraps the tool args under
|
|
61
|
-
# tool_input; for robustness we also look at the top level. We concatenate
|
|
62
|
-
# every plausible content field so a banned phrase anywhere is caught.
|
|
63
|
-
# `params` is where org_rpc carries an outbound body; `bodyText`/`subject` are
|
|
64
|
-
# the mail-desk (email_draft_send / email_send) content fields.
|
|
65
|
-
CONTENT=$(printf '%s' "$TOOL_INPUT" | jq -r '
|
|
66
|
-
[ .tool_input // {} , ( .tool_input.params? | if type=="object" then . else {} end ) , . ]
|
|
67
|
-
| map(
|
|
68
|
-
( .text? // empty ),
|
|
69
|
-
( .message? // empty ),
|
|
70
|
-
( .body? // empty ),
|
|
71
|
-
( .bodyText? // empty ),
|
|
72
|
-
( .subject? // empty ),
|
|
73
|
-
( .content? // empty ),
|
|
74
|
-
( .blocks? | if type=="array" or type=="object" then tojson else empty end )
|
|
75
|
-
)
|
|
76
|
-
| map(if type=="string" then . else tojson end)
|
|
77
|
-
| flatten
|
|
78
|
-
| map(select(. != null and . != ""))
|
|
79
|
-
| join("\n")
|
|
80
|
-
' 2>/dev/null) || deny "Failed to parse outbound content (fail closed)"
|
|
81
|
-
|
|
82
|
-
# Extract the recipient for the allowlist sanity check (optional field).
|
|
83
|
-
RECIPIENT=$(printf '%s' "$TOOL_INPUT" | jq -r '
|
|
84
|
-
( .tool_input // {} ) as $ti
|
|
85
|
-
| ( $ti.to? // $ti.recipient? // $ti.channel? // $ti.email? // $ti.thread_ts? // .to? // .recipient? // empty )
|
|
86
|
-
| if . == null then "" else (. | tostring) end
|
|
87
|
-
' 2>/dev/null) || deny "Failed to parse recipient (fail closed)"
|
|
88
|
-
|
|
89
|
-
# --- (a) banned-phrase screen ----------------------------------------------
|
|
90
|
-
# Source of truth: policies/communication-style.md (kept in sync manually).
|
|
91
|
-
# Matching is case-insensitive and substring-based. Opener phrases
|
|
92
|
-
# ("Certainly!" / "Absolutely!") are common in legitimate prose mid-sentence,
|
|
93
|
-
# so we anchor those to the start of the content / a line.
|
|
94
|
-
BANNED_SUBSTRINGS=(
|
|
95
|
-
"I'm here to help"
|
|
96
|
-
"I am here to help"
|
|
97
|
-
"Let me know if you need anything else"
|
|
98
|
-
"I hope this helps"
|
|
99
|
-
"Feel free to ask if you have any questions"
|
|
100
|
-
"As an AI"
|
|
101
|
-
"As a language model"
|
|
102
|
-
"I should clarify that I am"
|
|
103
|
-
"Great question!"
|
|
104
|
-
"Here's a comprehensive overview of"
|
|
105
|
-
"Here is a comprehensive overview of"
|
|
106
|
-
"I'd be happy to"
|
|
107
|
-
"I would be happy to"
|
|
108
|
-
)
|
|
40
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" || deny "cannot locate the hook directory (fail closed)"
|
|
41
|
+
GATE="$HERE/../../lib/comms/send-gate.mjs"
|
|
42
|
+
[ -f "$GATE" ] || deny "send gate module missing at $GATE (fail closed)"
|
|
109
43
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
for
|
|
114
|
-
|
|
115
|
-
case "$CONTENT_LC" in
|
|
116
|
-
*"$phrase_lc"*)
|
|
117
|
-
deny "Banned AI-tell phrase in outbound message: \"$phrase\" (see policies/communication-style.md)"
|
|
118
|
-
;;
|
|
119
|
-
esac
|
|
120
|
-
done
|
|
121
|
-
|
|
122
|
-
# Opener-only phrases: block when they begin the message or any line.
|
|
123
|
-
# (Mid-sentence "absolutely" is fine; "Absolutely!" as a line opener is not.)
|
|
124
|
-
if printf '%s' "$CONTENT" | grep -Eiq '(^|[[:cntrl:]])[[:space:]]*(Certainly!|Absolutely!|Great question!)'; then
|
|
125
|
-
deny "Banned filler opener (Certainly!/Absolutely!/Great question!) — see policies/communication-style.md"
|
|
126
|
-
fi
|
|
127
|
-
|
|
128
|
-
# --- (a2) persona-leak screen ----------------------------------------------
|
|
129
|
-
# The agent is one persona to the humans it talks to; parallel sessions,
|
|
130
|
-
# sub-sessions, subagents and workflows are internal workings. Outbound text
|
|
131
|
-
# may not name them, and may not name Claude Code / a language model. The
|
|
132
|
-
# denial carries the offending phrase so the session rewrites in its own voice
|
|
133
|
-
# ("my team", "a colleague", "one of my analysts").
|
|
134
|
-
#
|
|
135
|
-
# Patterns are case-insensitive extended regexes. "subagent"/"sub-agent" is
|
|
136
|
-
# blocked only when it is followed by said|reported|session — the bare noun in
|
|
137
|
-
# an internal-process sentence is tolerated, the attribution of speech to a
|
|
138
|
-
# machine part is not.
|
|
139
|
-
PERSONA_PATTERNS=(
|
|
140
|
-
'claude[[:space:]]+code'
|
|
141
|
-
'claude[[:space:]]+sessions?'
|
|
142
|
-
'sub-sessions?'
|
|
143
|
-
'sub-?agents?[[:space:]]+(said|reported|sessions?)'
|
|
144
|
-
'workflow[[:space:]]+agents?'
|
|
145
|
-
'as[[:space:]]+an[[:space:]]+ai\b'
|
|
146
|
-
'language[[:space:]]+model'
|
|
147
|
-
)
|
|
148
|
-
for pat in "${PERSONA_PATTERNS[@]}"; do
|
|
149
|
-
hit=$(printf '%s' "$CONTENT" | grep -Eio -- "$pat" 2>/dev/null | head -n 1) || true
|
|
150
|
-
if [ -n "$hit" ]; then
|
|
151
|
-
deny "Persona leak in outbound message: \"$hit\" — rewrite in your own voice (my team / a colleague), never name the machinery"
|
|
152
|
-
fi
|
|
153
|
-
done
|
|
154
|
-
|
|
155
|
-
# Session names are internal identifiers and must never reach a human — but
|
|
156
|
-
# the rule is BOUNDED, not guessed: `<first>-main` is always internal; any
|
|
157
|
-
# other name is internal only when it is a registered peer
|
|
158
|
-
# ($AGENT_ROOT/state/session/peers.json, written by `maestro session spawn`,
|
|
159
|
-
# an array or {peers:[…]} of {name,…}) or listed in MAESTRO_SESSION_NAMES
|
|
160
|
-
# (comma-separated). A colleague called Marc-Antoine, the slug james-kirkland
|
|
161
|
-
# in a URL or the mailbox ethan-miller@ are ordinary text.
|
|
162
|
-
#
|
|
163
|
-
# The first name resolves, in order, from MAESTRO_AGENT_FIRST,
|
|
164
|
-
# $AGENT_ROOT/config/agent.json (firstName, else the first word of fullName)
|
|
165
|
-
# and the machine pointer ~/.claude/maestro-agent.json (agentName). When none
|
|
166
|
-
# resolves the `<first>-main` check is skipped — the phrase gate above still
|
|
167
|
-
# applies — because a wrong guess would block legitimate text.
|
|
168
|
-
FIRST="${MAESTRO_AGENT_FIRST:-}"
|
|
169
|
-
if [ -z "$FIRST" ]; then
|
|
170
|
-
for cand in "${AGENT_ROOT:-}/config/agent.json" "${AGENT_DIR:-}/config/agent.json" "$HOME/.claude/maestro-agent.json"; do
|
|
171
|
-
[ -f "$cand" ] || continue
|
|
172
|
-
FIRST=$(jq -r '(.firstName // .agentName // .fullName // "") | tostring | split(" ")[0] | ascii_downcase' "$cand" 2>/dev/null) || FIRST=""
|
|
173
|
-
[ -n "$FIRST" ] && [ "$FIRST" != "null" ] && break
|
|
174
|
-
FIRST=""
|
|
44
|
+
NODE="$(command -v node 2>/dev/null || true)"
|
|
45
|
+
if [ -z "$NODE" ]; then
|
|
46
|
+
# launchd and some hook runners carry a bare PATH.
|
|
47
|
+
for cand in /opt/homebrew/bin/node /usr/local/bin/node "${HOME:-}/.local/bin/node" /usr/bin/node; do
|
|
48
|
+
if [ -x "$cand" ]; then NODE="$cand"; break; fi
|
|
175
49
|
done
|
|
176
50
|
fi
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
SESSION_NAMES=()
|
|
180
|
-
[ -n "$FIRST" ] && SESSION_NAMES+=("${FIRST}-main")
|
|
181
|
-
for peers in "${AGENT_ROOT:-}/state/session/peers.json" "${AGENT_DIR:-}/state/session/peers.json"; do
|
|
182
|
-
[ -f "$peers" ] || continue
|
|
183
|
-
# A corrupt registry is an empty one: this must never fail the whole send closed.
|
|
184
|
-
while IFS= read -r pn; do
|
|
185
|
-
[ -n "$pn" ] && SESSION_NAMES+=("$pn")
|
|
186
|
-
done < <(jq -r '(if type=="array" then . elif type=="object" then (.peers // []) else [] end) | .[] | select(type=="object") | (.name // empty) | tostring | ascii_downcase' "$peers" 2>/dev/null || true)
|
|
187
|
-
break
|
|
188
|
-
done
|
|
189
|
-
if [ -n "${MAESTRO_SESSION_NAMES:-}" ]; then
|
|
190
|
-
IFS=',' read -r -a extra_names <<< "$MAESTRO_SESSION_NAMES"
|
|
191
|
-
# bash 3.2 (macOS) + set -u: an empty array expands as "unbound" — use the ${a[@]+"${a[@]}"} idiom.
|
|
192
|
-
for en in ${extra_names[@]+"${extra_names[@]}"}; do
|
|
193
|
-
en=$(printf '%s' "$en" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
|
|
194
|
-
[ -n "$en" ] && SESSION_NAMES+=("$en")
|
|
195
|
-
done
|
|
196
|
-
fi
|
|
197
|
-
for sname in ${SESSION_NAMES[@]+"${SESSION_NAMES[@]}"}; do
|
|
198
|
-
sname_re=$(printf '%s' "$sname" | sed 's/[.[\*^$()+?{|]/\\&/g')
|
|
199
|
-
# Word-bounded on [A-Za-z0-9_-]: "alex-main" hits, "alex-mainland" / "alex-main-street" do not.
|
|
200
|
-
name_hit=$(printf '%s' "$CONTENT" | grep -Eio -- "(^|[^[:alnum:]_-])${sname_re}([^[:alnum:]_-]|$)" 2>/dev/null | head -n 1 | sed -E 's/^[^[:alnum:]_-]//; s/[^[:alnum:]_-]$//') || true
|
|
201
|
-
if [ -n "$name_hit" ]; then
|
|
202
|
-
deny "Persona leak in outbound message: \"$name_hit\" is an internal session name — say \"my team\" or \"a colleague\" instead"
|
|
203
|
-
fi
|
|
204
|
-
done
|
|
205
|
-
|
|
206
|
-
# --- (b) recipient allowlist sanity check ----------------------------------
|
|
207
|
-
# Only enforced when a recipient is present AND an allowlist file exists.
|
|
208
|
-
# Allowlist file: one entry (channel id, email, handle) per line, '#' comments.
|
|
209
|
-
ALLOWLIST_FILE="${MAESTRO_SEND_ALLOWLIST:-}"
|
|
210
|
-
if [ -n "$RECIPIENT" ] && [ -n "$ALLOWLIST_FILE" ] && [ -f "$ALLOWLIST_FILE" ]; then
|
|
211
|
-
if ! grep -Eq -- "^[[:space:]]*$(printf '%s' "$RECIPIENT" | sed 's/[.[\*^$()+?{|]/\\&/g')[[:space:]]*(#.*)?$" "$ALLOWLIST_FILE" 2>/dev/null; then
|
|
212
|
-
deny "Recipient \"$RECIPIENT\" not on send allowlist ($ALLOWLIST_FILE)"
|
|
213
|
-
fi
|
|
214
|
-
fi
|
|
215
|
-
|
|
216
|
-
# --- (c) rate caps (hourly + daily) ----------------------------------------
|
|
217
|
-
RUNTIME_DIR="${MAESTRO_RUNTIME_DIR:-$HOME/.maestro/runtime}/audit"
|
|
218
|
-
if ! mkdir -p "$RUNTIME_DIR" 2>/dev/null; then
|
|
219
|
-
deny "Cannot create audit runtime dir $RUNTIME_DIR (fail closed)"
|
|
220
|
-
fi
|
|
221
|
-
|
|
222
|
-
COUNTER_FILE="$RUNTIME_DIR/send-counter.tsv"
|
|
223
|
-
TODAY=$(date +%Y-%m-%d)
|
|
224
|
-
HOUR=$(date +%H)
|
|
225
|
-
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
226
|
-
SEND_LOG="$RUNTIME_DIR/$TODAY-sends.jsonl"
|
|
227
|
-
|
|
228
|
-
PER_HOUR_CAP="${MAESTRO_SEND_PER_HOUR:-3000}"
|
|
229
|
-
PER_DAY_CAP="${MAESTRO_SEND_PER_DAY:-20000}"
|
|
230
|
-
|
|
231
|
-
# Counter format (TSV): date<TAB>hour<TAB>hour_count<TAB>day_count
|
|
232
|
-
# Reset whenever the date rolls over; reset the hour bucket when hour rolls.
|
|
233
|
-
PREV_DATE=""; PREV_HOUR=""; HOUR_COUNT=0; DAY_COUNT=0
|
|
234
|
-
if [ -f "$COUNTER_FILE" ]; then
|
|
235
|
-
IFS=$'\t' read -r PREV_DATE PREV_HOUR HOUR_COUNT DAY_COUNT < "$COUNTER_FILE" 2>/dev/null || true
|
|
236
|
-
fi
|
|
237
|
-
# Sanitise to integers (fail closed to 0 rather than aborting).
|
|
238
|
-
case "$HOUR_COUNT" in (''|*[!0-9]*) HOUR_COUNT=0 ;; esac
|
|
239
|
-
case "$DAY_COUNT" in (''|*[!0-9]*) DAY_COUNT=0 ;; esac
|
|
240
|
-
|
|
241
|
-
if [ "$PREV_DATE" != "$TODAY" ]; then
|
|
242
|
-
DAY_COUNT=0
|
|
243
|
-
HOUR_COUNT=0
|
|
244
|
-
elif [ "$PREV_HOUR" != "$HOUR" ]; then
|
|
245
|
-
HOUR_COUNT=0
|
|
246
|
-
fi
|
|
247
|
-
|
|
248
|
-
if [ "$DAY_COUNT" -ge "$PER_DAY_CAP" ]; then
|
|
249
|
-
deny "Daily send limit ($PER_DAY_CAP) reached. Sends resume tomorrow."
|
|
250
|
-
fi
|
|
251
|
-
if [ "$HOUR_COUNT" -ge "$PER_HOUR_CAP" ]; then
|
|
252
|
-
deny "Hourly send limit ($PER_HOUR_CAP) reached. Sends resume next hour."
|
|
253
|
-
fi
|
|
254
|
-
|
|
255
|
-
# --- commit: increment counters + log allowed send -------------------------
|
|
256
|
-
HOUR_COUNT=$((HOUR_COUNT + 1))
|
|
257
|
-
DAY_COUNT=$((DAY_COUNT + 1))
|
|
258
|
-
if ! printf '%s\t%s\t%s\t%s\n' "$TODAY" "$HOUR" "$HOUR_COUNT" "$DAY_COUNT" > "$COUNTER_FILE" 2>/dev/null; then
|
|
259
|
-
deny "Cannot write send counter (fail closed)"
|
|
260
|
-
fi
|
|
261
|
-
|
|
262
|
-
RECIPIENT_JSON=$(printf '%s' "$RECIPIENT" | jq -Rs . 2>/dev/null || printf '""')
|
|
263
|
-
printf '{"timestamp":"%s","channel":"%s","recipient":%s,"status":"allowed"}\n' \
|
|
264
|
-
"$TIMESTAMP" "$CHANNEL_TYPE" "$RECIPIENT_JSON" >> "$SEND_LOG" 2>/dev/null || true
|
|
51
|
+
[ -n "$NODE" ] || deny "node not available — cannot screen outbound content (fail closed)"
|
|
265
52
|
|
|
266
|
-
|
|
267
|
-
|
|
53
|
+
"$NODE" "$GATE" --pre-send-hook "${1:-}"
|
|
54
|
+
rc=$?
|
|
55
|
+
case "$rc" in
|
|
56
|
+
0|2) exit "$rc" ;;
|
|
57
|
+
*) echo "BLOCKED: pre-send gate exited $rc (fail closed)" >&2; exit 2 ;;
|
|
58
|
+
esac
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* pre-write-yaml-validate.mjs — PreToolUse gate on Write|Edit for agent state YAML.
|
|
4
|
+
*
|
|
5
|
+
* Why: the queue files under state/ are the agent's memory of what it owes whom. Three
|
|
6
|
+
* failure shapes have each cost a seat hours: (1) a Write that replaced a 119-row queue with
|
|
7
|
+
* the 6 rows the writer had in view (a clobber); (2) an Edit whose old_string matched twice
|
|
8
|
+
* and landed on the wrong row; (3) a plain scalar carrying an unquoted " #" that parsed
|
|
9
|
+
* cleanly and silently lost the rest of the value — the only trace is a provenance field
|
|
10
|
+
* ending on a dangling connector ("... @mention in"). All three pass a syntax check after the
|
|
11
|
+
* fact; only a gate that sees the WOULD-BE content before the write can refuse them.
|
|
12
|
+
*
|
|
13
|
+
* Contract (Claude Code PreToolUse): reads the hook payload on stdin —
|
|
14
|
+
* { tool_name, tool_input: { file_path, content | old_string, new_string, replace_all } }
|
|
15
|
+
* Acts only on paths under state/queues, state/dashboards or state/indexes ending in .yaml
|
|
16
|
+
* (.yml). Computes the would-be file in memory (Write: `content`; Edit: the on-disk file with
|
|
17
|
+
* old_string replaced by new_string) and REFUSES — exit 2, a permissionDecision:"deny" JSON on
|
|
18
|
+
* stdout and the reason on stderr — when:
|
|
19
|
+
* - an Edit's old_string is absent from the file, or matches more than once without
|
|
20
|
+
* replace_all (the edit would land somewhere the writer did not look at);
|
|
21
|
+
* - the result does not parse as YAML (js-yaml, which also rejects duplicate keys);
|
|
22
|
+
* - a provenance-keyed value NEW in this write ends on a dangling connector. The delta is
|
|
23
|
+
* judged, not the whole file, so a pre-existing truncated row never blocks an unrelated
|
|
24
|
+
* write — the PostToolUse backstop (validate-state-yaml.sh) still reports it;
|
|
25
|
+
* - the top-level `items:` row count would shrink versus the file on disk (or a top-level
|
|
26
|
+
* sequence would get shorter), unless MAESTRO_ALLOW_ROW_SHRINK=1 is set for a deliberate
|
|
27
|
+
* archive/prune. Other list keys (dashboard sections, index tables) are regenerated and
|
|
28
|
+
* legitimately shrink, so they are not guarded.
|
|
29
|
+
* Everything else exits 0. Tooling gaps (no stdin, unparseable payload, js-yaml missing)
|
|
30
|
+
* fail OPEN with a note on stderr: a gate that strands every write on a missing dependency
|
|
31
|
+
* costs more than the clobber it prevents. Paths are resolved against $CLAUDE_PROJECT_DIR,
|
|
32
|
+
* else the working directory.
|
|
33
|
+
*
|
|
34
|
+
* The evaluation is a pure function (`evaluate`) so it is unit-tested without a harness;
|
|
35
|
+
* the CLI wrapper at the bottom only does stdin/stdout/exit.
|
|
36
|
+
*
|
|
37
|
+
* @module scripts/hooks/pre-write-yaml-validate
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
41
|
+
import { resolve, isAbsolute } from "node:path";
|
|
42
|
+
import { createRequire } from "node:module";
|
|
43
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
44
|
+
|
|
45
|
+
/** Path segment test: any .yaml under one of the three state-bearing directories. */
|
|
46
|
+
export const SCOPE_RE = /(^|\/)state\/(queues|dashboards|indexes)\/[^/]+\.ya?ml$/;
|
|
47
|
+
|
|
48
|
+
/** Keys whose values name where a row came from or who owns it — short, so the connector test is close to exact. */
|
|
49
|
+
export const PROVENANCE_KEYS = Object.freeze([
|
|
50
|
+
"origin", "source", "source_ref", "sourceRef", "raised_by", "counterparty", "channel", "via",
|
|
51
|
+
"provenance", "escalated_to", "reported_by", "requested_by", "decided_by", "decision_owner",
|
|
52
|
+
"owner", "reviewer", "notify",
|
|
53
|
+
]);
|
|
54
|
+
const PROV_SET = new Set(PROVENANCE_KEYS);
|
|
55
|
+
|
|
56
|
+
/** A value ending on one of these is the signature of a comment-eaten scalar. */
|
|
57
|
+
export const CONNECTOR_RE = /\s(in|to|at|on|from|with|the|and|of|for|by)$/i;
|
|
58
|
+
|
|
59
|
+
/** Is this file_path one the gate cares about? */
|
|
60
|
+
export function inScope(filePath) {
|
|
61
|
+
if (typeof filePath !== "string" || !filePath) return false;
|
|
62
|
+
return SCOPE_RE.test(filePath.replace(/\\/g, "/"));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Load js-yaml from wherever the agent has it: next to this script (the agent repo's own
|
|
67
|
+
* node_modules), the project dir, or the working directory. Returns null when none resolves.
|
|
68
|
+
*/
|
|
69
|
+
export function loadYaml(bases = []) {
|
|
70
|
+
const candidates = [
|
|
71
|
+
fileURLToPath(new URL("./", import.meta.url)),
|
|
72
|
+
...bases.filter(Boolean),
|
|
73
|
+
process.cwd(),
|
|
74
|
+
];
|
|
75
|
+
for (const base of candidates) {
|
|
76
|
+
try {
|
|
77
|
+
const req = createRequire(pathToFileURL(base.endsWith("/") ? base : `${base}/`));
|
|
78
|
+
return req("js-yaml");
|
|
79
|
+
} catch { /* try the next base */ }
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Walk a parsed document and collect every provenance-keyed string ending on a connector. */
|
|
85
|
+
export function provenanceHits(doc) {
|
|
86
|
+
const hits = [];
|
|
87
|
+
// A nested map (history entry, sub-object) reports the nearest enclosing row id.
|
|
88
|
+
const walk = (node, inherited) => {
|
|
89
|
+
if (Array.isArray(node)) { for (const it of node) walk(it, inherited); return; }
|
|
90
|
+
if (!node || typeof node !== "object") return;
|
|
91
|
+
const rid = typeof node.id === "string" ? node.id : inherited;
|
|
92
|
+
for (const [k, v] of Object.entries(node)) {
|
|
93
|
+
if (typeof v === "string") {
|
|
94
|
+
if (PROV_SET.has(k) && v.length < 300 && CONNECTOR_RE.test(v.trimEnd())) hits.push({ id: rid, key: k, value: v });
|
|
95
|
+
} else if (v && typeof v === "object") {
|
|
96
|
+
walk(v, rid);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
walk(doc, "?");
|
|
101
|
+
return hits;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Row count the shrink guard protects: a top-level `items:` list, or a top-level sequence. */
|
|
105
|
+
export function guardedRowCount(doc) {
|
|
106
|
+
if (Array.isArray(doc)) return doc.length;
|
|
107
|
+
if (doc && typeof doc === "object" && Array.isArray(doc.items)) return doc.items.length;
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Count non-overlapping occurrences of `needle` in `hay`. */
|
|
112
|
+
function countOccurrences(hay, needle) {
|
|
113
|
+
if (!needle) return 0;
|
|
114
|
+
let n = 0;
|
|
115
|
+
let i = 0;
|
|
116
|
+
for (;;) {
|
|
117
|
+
const at = hay.indexOf(needle, i);
|
|
118
|
+
if (at < 0) return n;
|
|
119
|
+
n += 1;
|
|
120
|
+
i = at + needle.length;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Compute the would-be content for the tool call. Returns { content } or { refuse }.
|
|
126
|
+
* @param {string} toolName
|
|
127
|
+
* @param {object} ti tool_input
|
|
128
|
+
* @param {string|null} current on-disk content (null when the file does not exist)
|
|
129
|
+
*/
|
|
130
|
+
export function wouldBeContent(toolName, ti, current) {
|
|
131
|
+
if (toolName === "Write") {
|
|
132
|
+
return { content: typeof ti.content === "string" ? ti.content : "" };
|
|
133
|
+
}
|
|
134
|
+
if (toolName === "Edit") {
|
|
135
|
+
if (current === null) return { refuse: "Edit on a file that does not exist" };
|
|
136
|
+
const oldStr = ti.old_string;
|
|
137
|
+
const newStr = typeof ti.new_string === "string" ? ti.new_string : "";
|
|
138
|
+
if (typeof oldStr !== "string" || oldStr === "") return { refuse: "Edit with an empty old_string" };
|
|
139
|
+
const n = countOccurrences(current, oldStr);
|
|
140
|
+
if (n === 0) return { refuse: "old_string is not present in the file on disk — the edit would not land where you looked" };
|
|
141
|
+
if (n > 1 && !ti.replace_all) return { refuse: `old_string matches ${n} places — anchor it to one row or pass replace_all` };
|
|
142
|
+
return { content: ti.replace_all ? current.split(oldStr).join(newStr) : current.replace(oldStr, () => newStr) };
|
|
143
|
+
}
|
|
144
|
+
if (toolName === "MultiEdit" && Array.isArray(ti.edits)) {
|
|
145
|
+
let content = current === null ? "" : current;
|
|
146
|
+
for (const e of ti.edits) {
|
|
147
|
+
const r = wouldBeContent("Edit", e || {}, content);
|
|
148
|
+
if (r.refuse) return r;
|
|
149
|
+
content = r.content;
|
|
150
|
+
}
|
|
151
|
+
return { content };
|
|
152
|
+
}
|
|
153
|
+
return { content: null };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Pure evaluation of one hook payload.
|
|
158
|
+
*
|
|
159
|
+
* @param {object} payload the hook JSON ({tool_name, tool_input})
|
|
160
|
+
* @param {object} [o]
|
|
161
|
+
* @param {string} [o.projectDir] base for relative file_path (default: cwd)
|
|
162
|
+
* @param {object} [o.env] environment (default: process.env)
|
|
163
|
+
* @param {(p:string)=>string|null} [o.readFile] test seam; returns null when absent
|
|
164
|
+
* @param {object} [o.yaml] js-yaml module (test seam)
|
|
165
|
+
* @returns {{ decision:"allow"|"deny"|"skip", reason?:string }}
|
|
166
|
+
*/
|
|
167
|
+
export function evaluate(payload, o = {}) {
|
|
168
|
+
const env = o.env || process.env;
|
|
169
|
+
const projectDir = o.projectDir || env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
170
|
+
if (!payload || typeof payload !== "object") return { decision: "skip", reason: "no payload" };
|
|
171
|
+
const toolName = String(payload.tool_name || "");
|
|
172
|
+
const ti = payload.tool_input && typeof payload.tool_input === "object" ? payload.tool_input : {};
|
|
173
|
+
if (!["Write", "Edit", "MultiEdit"].includes(toolName)) return { decision: "skip" };
|
|
174
|
+
const fp = typeof ti.file_path === "string" ? ti.file_path : "";
|
|
175
|
+
if (!inScope(fp)) return { decision: "skip" };
|
|
176
|
+
const abs = isAbsolute(fp) ? fp : resolve(projectDir, fp);
|
|
177
|
+
|
|
178
|
+
const readFile = o.readFile || ((p) => (existsSync(p) ? readFileSync(p, "utf-8") : null));
|
|
179
|
+
let current = null;
|
|
180
|
+
try { current = readFile(abs); } catch { current = null; }
|
|
181
|
+
|
|
182
|
+
const wb = wouldBeContent(toolName, ti, current);
|
|
183
|
+
if (wb.refuse) return { decision: "deny", reason: `${toolName} ${fp}: ${wb.refuse}` };
|
|
184
|
+
if (wb.content === null) return { decision: "skip" };
|
|
185
|
+
|
|
186
|
+
const yaml = o.yaml || loadYaml([projectDir]);
|
|
187
|
+
if (!yaml) return { decision: "skip", reason: "js-yaml not resolvable — gate skipped (fail-open)" };
|
|
188
|
+
|
|
189
|
+
let next;
|
|
190
|
+
try {
|
|
191
|
+
next = yaml.load(wb.content);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
const msg = (err && err.message ? err.message : String(err)).split("\n")[0].slice(0, 300);
|
|
194
|
+
return { decision: "deny", reason: `${toolName} ${fp}: the result would not parse as YAML — ${msg}. Quote scalars carrying ':' or ' #', and never append a second copy of a key.` };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let prev;
|
|
198
|
+
let prevParsed = false;
|
|
199
|
+
if (current !== null) {
|
|
200
|
+
try { prev = yaml.load(current); prevParsed = true; } catch { prevParsed = false; }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Provenance truncation — judged on the DELTA so pre-existing rows never block a new write.
|
|
204
|
+
const before = new Set(prevParsed ? provenanceHits(prev).map((h) => `${h.id}\u0000${h.key}\u0000${h.value}`) : []);
|
|
205
|
+
const fresh = provenanceHits(next).filter((h) => !before.has(`${h.id}\u0000${h.key}\u0000${h.value}`));
|
|
206
|
+
if (fresh.length) {
|
|
207
|
+
const shown = fresh.slice(0, 5).map((h) => `${h.id} ${h.key}: ...${h.value.slice(-60)}`).join("; ");
|
|
208
|
+
return {
|
|
209
|
+
decision: "deny",
|
|
210
|
+
reason: `${toolName} ${fp}: provenance field ends on a dangling connector (an unquoted ' #' silently truncates a plain scalar) — ${shown}. Quote the scalar and write the whole value.`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Row-count shrink guard.
|
|
215
|
+
if (prevParsed && env.MAESTRO_ALLOW_ROW_SHRINK !== "1") {
|
|
216
|
+
const was = guardedRowCount(prev);
|
|
217
|
+
const now = guardedRowCount(next);
|
|
218
|
+
if (was !== null && now !== null && now < was) {
|
|
219
|
+
return {
|
|
220
|
+
decision: "deny",
|
|
221
|
+
reason: `${toolName} ${fp}: top-level items would shrink from ${was} to ${now} rows — a write that drops rows is almost always a clobber of rows you did not have in view. Re-read the file and edit the one row, or set MAESTRO_ALLOW_ROW_SHRINK=1 for a deliberate archive.`,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { decision: "allow" };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Claude Code hook output for a refusal (exit 2 + this JSON on stdout). */
|
|
230
|
+
export function denyOutput(reason) {
|
|
231
|
+
return {
|
|
232
|
+
decision: "block",
|
|
233
|
+
reason,
|
|
234
|
+
hookSpecificOutput: {
|
|
235
|
+
hookEventName: "PreToolUse",
|
|
236
|
+
permissionDecision: "deny",
|
|
237
|
+
permissionDecisionReason: reason,
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function readStdin() {
|
|
243
|
+
const chunks = [];
|
|
244
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
245
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function main() {
|
|
249
|
+
let payload;
|
|
250
|
+
try {
|
|
251
|
+
const raw = await readStdin();
|
|
252
|
+
if (!raw.trim()) process.exit(0);
|
|
253
|
+
payload = JSON.parse(raw);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
process.stderr.write(`pre-write-yaml-validate: unreadable hook payload (${err && err.message}) — gate skipped\n`);
|
|
256
|
+
process.exit(0);
|
|
257
|
+
}
|
|
258
|
+
let verdict;
|
|
259
|
+
try {
|
|
260
|
+
verdict = evaluate(payload);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
process.stderr.write(`pre-write-yaml-validate: internal error (${err && err.message}) — gate skipped\n`);
|
|
263
|
+
process.exit(0);
|
|
264
|
+
}
|
|
265
|
+
if (verdict.decision === "deny") {
|
|
266
|
+
process.stdout.write(`${JSON.stringify(denyOutput(verdict.reason))}\n`);
|
|
267
|
+
process.stderr.write(`BLOCKED by pre-write-yaml-validate: ${verdict.reason}\n`);
|
|
268
|
+
process.exit(2);
|
|
269
|
+
}
|
|
270
|
+
if (verdict.decision === "skip" && verdict.reason) process.stderr.write(`pre-write-yaml-validate: ${verdict.reason}\n`);
|
|
271
|
+
process.exit(0);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const invokedDirectly = process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
275
|
+
if (invokedDirectly) main();
|