@cohortapp/agent-sdk 2.17.0 → 2.18.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +18 -0
- package/.env.example +18 -5
- package/README.md +1 -0
- package/bin/maestro.mjs +62 -0
- package/docs/guides/billing-console-keys.md +60 -0
- package/docs/guides/front-door-session.md +54 -9
- package/docs/guides/mac-mini.md +20 -25
- package/docs/guides/setup-wizard.md +1 -1
- package/docs/runbooks/fleet-rollout.md +156 -0
- package/docs/runbooks/mac-mini-bootstrap.md +12 -14
- package/lib/action-executor.js +19 -3
- package/lib/budget-guard.mjs +279 -3
- package/lib/channels/base-adapter.mjs +3 -1
- package/lib/channels/contract.mjs +2 -1
- package/lib/channels/inbox-item.mjs +8 -0
- package/lib/claude-bin.mjs +5 -6
- package/lib/cli/doctor-checks.mjs +141 -10
- package/lib/cli/global-setup-extras.mjs +5 -1
- package/lib/cli/inbox.mjs +100 -15
- package/lib/cli/seat-auth.mjs +463 -0
- package/lib/cli/session.mjs +80 -12
- package/lib/collective/capture.mjs +8 -6
- package/lib/collective/global-config.mjs +63 -1
- package/lib/collective/presence.mjs +142 -5
- package/lib/comms/send-gate.mjs +559 -1
- package/lib/diagnostics/alerts.mjs +49 -0
- package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
- package/lib/engine/agents/definitions.mjs +343 -0
- package/lib/engine/agents/persist.mjs +275 -0
- package/lib/engine/agents/runtime.mjs +748 -0
- package/lib/engine/agents/usage.mjs +95 -0
- package/lib/engine/auth-status.mjs +139 -0
- package/lib/engine/budget.mjs +194 -0
- package/lib/engine/cli.mjs +1204 -0
- package/lib/engine/commands/index.mjs +269 -0
- package/lib/engine/context/budget.mjs +219 -0
- package/lib/engine/context/cache.mjs +125 -0
- package/lib/engine/context/child-env.mjs +215 -0
- package/lib/engine/context/compaction.mjs +342 -0
- package/lib/engine/context/images.mjs +90 -0
- package/lib/engine/context/instructions.mjs +327 -0
- package/lib/engine/context/lazy-instructions.mjs +169 -0
- package/lib/engine/context/manager.mjs +182 -0
- package/lib/engine/context/real-path.mjs +91 -0
- package/lib/engine/context/secret-values.mjs +163 -0
- package/lib/engine/context/settings.mjs +274 -0
- package/lib/engine/context/stream-input.mjs +159 -0
- package/lib/engine/guard.mjs +152 -0
- package/lib/engine/hooks.mjs +713 -0
- package/lib/engine/loop.mjs +560 -0
- package/lib/engine/mcp/client.mjs +254 -0
- package/lib/engine/mcp/config.mjs +301 -0
- package/lib/engine/mcp/http.mjs +201 -0
- package/lib/engine/mcp/index.mjs +146 -0
- package/lib/engine/mcp/jsonrpc.mjs +147 -0
- package/lib/engine/mcp/naming.mjs +66 -0
- package/lib/engine/mcp/resources.mjs +89 -0
- package/lib/engine/mcp/results.mjs +133 -0
- package/lib/engine/mcp/stdio.mjs +137 -0
- package/lib/engine/mcp/supervisor.mjs +116 -0
- package/lib/engine/messages.mjs +104 -0
- package/lib/engine/output/json.mjs +164 -0
- package/lib/engine/output/stream-json.mjs +266 -0
- package/lib/engine/permissions.mjs +845 -0
- package/lib/engine/process-identity.mjs +164 -0
- package/lib/engine/process-tree.mjs +551 -0
- package/lib/engine/prompt.mjs +60 -0
- package/lib/engine/session/store.mjs +299 -0
- package/lib/engine/session-runtime/args.mjs +97 -0
- package/lib/engine/session-runtime/host.mjs +143 -0
- package/lib/engine/session-runtime/inbox.mjs +122 -0
- package/lib/engine/session-runtime/notifications.mjs +129 -0
- package/lib/engine/session-runtime/registry.mjs +328 -0
- package/lib/engine/session-runtime/runner.mjs +344 -0
- package/lib/engine/session-runtime/socket.mjs +212 -0
- package/lib/engine/session-runtime/wakeup.mjs +115 -0
- package/lib/engine/skills/index.mjs +321 -0
- package/lib/engine/tools/bash-background.mjs +533 -0
- package/lib/engine/tools/bash.mjs +216 -0
- package/lib/engine/tools/edit.mjs +97 -0
- package/lib/engine/tools/glob.mjs +81 -0
- package/lib/engine/tools/grep.mjs +224 -0
- package/lib/engine/tools/index.mjs +84 -0
- package/lib/engine/tools/list-agents.mjs +32 -0
- package/lib/engine/tools/ls.mjs +127 -0
- package/lib/engine/tools/monitor.mjs +82 -0
- package/lib/engine/tools/notebook-edit.mjs +218 -0
- package/lib/engine/tools/read.mjs +103 -0
- package/lib/engine/tools/schedule-wakeup.mjs +45 -0
- package/lib/engine/tools/schema.mjs +144 -0
- package/lib/engine/tools/send-message.mjs +77 -0
- package/lib/engine/tools/session.mjs +70 -0
- package/lib/engine/tools/todo.mjs +144 -0
- package/lib/engine/tools/toolsearch.mjs +217 -0
- package/lib/engine/tools/walk.mjs +193 -0
- package/lib/engine/tools/web-switch.mjs +31 -0
- package/lib/engine/tools/webfetch-html.mjs +387 -0
- package/lib/engine/tools/webfetch-net.mjs +340 -0
- package/lib/engine/tools/webfetch.mjs +198 -0
- package/lib/engine/tools/websearch.mjs +91 -0
- package/lib/engine/tools/workflow.mjs +95 -0
- package/lib/engine/tools/write.mjs +76 -0
- package/lib/engine/tui/line-editor.mjs +137 -0
- package/lib/engine/tui/render.mjs +86 -0
- package/lib/engine/tui/tui.mjs +274 -0
- package/lib/engine/wire/anthropic-messages.mjs +263 -0
- package/lib/engine/wire/effort.mjs +36 -0
- package/lib/engine/wire/errors.mjs +496 -0
- package/lib/engine/wire/http.mjs +441 -0
- package/lib/engine/wire/index.mjs +76 -0
- package/lib/engine/wire/openai-chat.mjs +332 -0
- package/lib/engine/wire/prompt-cache.mjs +79 -0
- package/lib/engine/wire/search.mjs +140 -0
- package/lib/engine/wire/sse.mjs +114 -0
- package/lib/engine/wire/stall.mjs +349 -0
- package/lib/engine/wire/token-provider.mjs +175 -0
- package/lib/engine/wire/usage.mjs +192 -0
- package/lib/engine/workflow/host.mjs +524 -0
- package/lib/engine/workflow/journal.mjs +188 -0
- package/lib/engine/workflow/json-schema.mjs +171 -0
- package/lib/engine/workflow/meta.mjs +329 -0
- package/lib/engine/workflow/notifications.mjs +52 -0
- package/lib/engine/workflow/runtime.mjs +447 -0
- package/lib/engine/workflow/sandbox.mjs +534 -0
- package/lib/engine/workflow/worker.mjs +141 -0
- package/lib/engine/workflow/worktree.mjs +74 -0
- package/lib/execution/disposition.mjs +1 -1
- package/lib/execution/intake.mjs +10 -0
- package/lib/execution/surface-policy.mjs +15 -0
- package/lib/learning/curator.mjs +8 -6
- package/lib/learning/reflect.mjs +8 -6
- package/lib/model-router/catalog/cohort.yaml +137 -0
- package/lib/model-router/catalog.mjs +118 -1
- package/lib/model-router/failover.mjs +67 -16
- package/lib/model-router/llm-task.mjs +39 -3
- package/lib/model-router/resolve.mjs +89 -3
- package/lib/model-router/spawn.mjs +46 -47
- package/lib/model-router/taxonomy.mjs +126 -4
- package/lib/org/cost-sync.mjs +141 -11
- package/lib/org/inbound/broadcast.mjs +289 -0
- package/lib/org/inbound/collective.mjs +375 -0
- package/lib/org/inbound/directedness.mjs +96 -8
- package/lib/org/inbound/facts.mjs +78 -2
- package/lib/org/inbound/project.mjs +22 -0
- package/lib/org/inbound/surfaces.mjs +14 -0
- package/lib/org/llm-token.mjs +879 -0
- package/lib/org/mesh.mjs +61 -0
- package/lib/org/messaging.mjs +3 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +15 -0
- package/lib/org/quota.mjs +520 -0
- package/lib/org/tool-surface.mjs +104 -16
- package/lib/org/ui-parity.mjs +16 -1
- package/lib/org/work-ledger.mjs +37 -6
- package/lib/rate-guard.mjs +114 -1
- package/lib/resource-governor.mjs +41 -6
- package/lib/runtime/adapter.mjs +823 -0
- package/lib/runtime/child-env.mjs +191 -0
- package/lib/runtime/legacy-shell-guard.mjs +97 -0
- package/lib/runtime/seat-engine.mjs +162 -0
- package/lib/session/ask-ledger.mjs +271 -0
- package/lib/session/current-work.mjs +676 -0
- package/lib/session/feed-core.mjs +40 -3
- package/lib/session/launch-args.mjs +56 -4
- package/lib/session/status-summary.mjs +26 -9
- package/lib/session/upgrade-notice.mjs +42 -0
- package/lib/setup/claude-probe.mjs +117 -13
- package/lib/setup/enrich.mjs +13 -10
- package/lib/setup/sections/model.mjs +39 -13
- package/lib/telemetry/collect.mjs +208 -9
- package/lib/upgrade/ignored-drift.mjs +105 -0
- package/lib/voice/post-call-brief.mjs +30 -17
- package/package.json +13 -3
- package/plugins/maestro-skills/skills/board-work.md +5 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
- package/plugins/maestro-skills/skills/main-session.md +18 -7
- package/scripts/ci/check-tarball-fidelity.mjs +126 -2
- package/scripts/ci/run-tests.mjs +47 -19
- package/scripts/cohort-llm/api-key-helper.mjs +92 -0
- package/scripts/collective/hook-runner.mjs +29 -2
- package/scripts/continuous-monitor.sh +13 -0
- package/scripts/cost/track-claude-usage.mjs +15 -0
- package/scripts/daemon/agent-daemon.mjs +408 -20
- package/scripts/daemon/assurance.mjs +48 -12
- package/scripts/daemon/cadence-consumer.mjs +218 -68
- package/scripts/daemon/cadence-handlers.mjs +73 -4
- package/scripts/daemon/classifier.mjs +75 -26
- package/scripts/daemon/context-compiler.mjs +51 -37
- package/scripts/daemon/deliver.mjs +30 -1
- package/scripts/daemon/dispatcher.mjs +595 -149
- package/scripts/daemon/health.mjs +14 -1
- package/scripts/daemon/maestro-daemon.mjs +11 -0
- package/scripts/daemon/prompt-builder.mjs +24 -0
- package/scripts/daemon/responder.mjs +246 -79
- package/scripts/daemon/sdk-version.mjs +98 -16
- package/scripts/eval/probe-gateway.mjs +635 -0
- package/scripts/eval/replay/extract.mjs +270 -0
- package/scripts/eval/replay/grade.mjs +260 -0
- package/scripts/eval/replay/lib/config.mjs +50 -0
- package/scripts/eval/replay/lib/effects.mjs +65 -0
- package/scripts/eval/replay/lib/fixture.mjs +188 -0
- package/scripts/eval/replay/lib/judge.mjs +72 -0
- package/scripts/eval/replay/lib/redact.mjs +136 -0
- package/scripts/eval/replay/lib/sandbox.mjs +170 -0
- package/scripts/eval/replay/lib/schema-check.mjs +63 -0
- package/scripts/eval/replay/lib/transcript.mjs +76 -0
- package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
- package/scripts/eval/replay/report.mjs +185 -0
- package/scripts/eval/replay/run.mjs +404 -0
- package/scripts/fleet/rollout.mjs +1094 -0
- package/scripts/hooks/pre-send-audit.sh +36 -245
- package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
- package/scripts/hooks/validate-state-yaml.sh +190 -0
- package/scripts/huddle/huddle-llm.mjs +361 -0
- package/scripts/huddle/huddle-server.mjs +46 -121
- package/scripts/local-triggers/autoupdate.sh +448 -78
- package/scripts/local-triggers/run-trigger.sh +13 -0
- package/scripts/maintenance/pin-integrity.mjs +364 -0
- package/scripts/poll-slack-events.sh +41 -9
- package/scripts/poller/slack-socket-mode.mjs +28 -3
- package/scripts/session/supervisor.mjs +80 -13
- package/scripts/spawn-session.sh +13 -0
- package/bin/maestro.test.mjs +0 -1574
- package/lib/action-executor.test.mjs +0 -871
- package/lib/archetype.test.mjs +0 -132
- package/lib/assurance/plan-note.test.mjs +0 -234
- package/lib/assurance/room-budget.test.mjs +0 -486
- package/lib/assurance/tier.test.mjs +0 -174
- package/lib/autonomy.test.mjs +0 -66
- package/lib/backlog.test.mjs +0 -302
- package/lib/backup/policy.test.mjs +0 -305
- package/lib/budget-escalate.test.mjs +0 -232
- package/lib/budget-guard.envelope.test.mjs +0 -476
- package/lib/budget-guard.test.mjs +0 -427
- package/lib/cadence-bus-requeue.test.mjs +0 -83
- package/lib/cadence-bus-schedule.test.mjs +0 -194
- package/lib/cadence-bus.test.mjs +0 -720
- package/lib/cadences.test.mjs +0 -230
- package/lib/capability/inventory.test.mjs +0 -232
- package/lib/capability.test.mjs +0 -78
- package/lib/channels/base-adapter.test.mjs +0 -590
- package/lib/channels/channels.test.mjs +0 -371
- package/lib/channels/contract.test.mjs +0 -162
- package/lib/channels/inbox-item.test.mjs +0 -368
- package/lib/channels/orgmail/adapter.test.mjs +0 -448
- package/lib/channels/pairing.test.mjs +0 -270
- package/lib/channels/repeat-suppressor.test.mjs +0 -134
- package/lib/channels/slack-adapter.test.mjs +0 -212
- package/lib/channels/telegram-adapter.test.mjs +0 -306
- package/lib/channels/voice/adapter.test.mjs +0 -278
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
- package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
- package/lib/charter.test.mjs +0 -89
- package/lib/claude-bin.test.mjs +0 -131
- package/lib/cli/board.test.mjs +0 -227
- package/lib/cli/design.test.mjs +0 -270
- package/lib/cli/doctor-checks.test.mjs +0 -336
- package/lib/cli/global-setup-extras.test.mjs +0 -462
- package/lib/cli/inbox.test.mjs +0 -230
- package/lib/cli/session-ack.test.mjs +0 -63
- package/lib/cli/session.test.mjs +0 -613
- package/lib/collective/capture.test.mjs +0 -121
- package/lib/collective/cards.test.mjs +0 -114
- package/lib/collective/config.test.mjs +0 -123
- package/lib/collective/global-config.test.mjs +0 -220
- package/lib/collective/global-skills.test.mjs +0 -126
- package/lib/collective/presence.test.mjs +0 -95
- package/lib/collective/recall.test.mjs +0 -116
- package/lib/collective/vendor-skills.test.mjs +0 -306
- package/lib/comms/send-gate.test.mjs +0 -770
- package/lib/comms.test.mjs +0 -41
- package/lib/context/budget.test.mjs +0 -252
- package/lib/context/history-scope.test.mjs +0 -79
- package/lib/cost/ledger-row.test.mjs +0 -183
- package/lib/design/design-md.test.mjs +0 -318
- package/lib/design/fixtures/DESIGN.golden.md +0 -238
- package/lib/design/fixtures/PRODUCT.golden.md +0 -67
- package/lib/design/fixtures/foundation.json +0 -133
- package/lib/design/refresh-gate.test.mjs +0 -144
- package/lib/design/write.test.mjs +0 -241
- package/lib/diagnostics/alerts.test.mjs +0 -318
- package/lib/diagnostics/backup-freshness.test.mjs +0 -185
- package/lib/diagnostics/counters.test.mjs +0 -206
- package/lib/diagnostics/events.test.mjs +0 -290
- package/lib/diagnostics/otel.test.mjs +0 -196
- package/lib/diagnostics/trace.test.mjs +0 -251
- package/lib/env-compat.test.mjs +0 -104
- package/lib/execution/disposition.test.mjs +0 -553
- package/lib/execution/drive.test.mjs +0 -270
- package/lib/execution/effects.test.mjs +0 -344
- package/lib/execution/intake.test.mjs +0 -389
- package/lib/execution/journal.test.mjs +0 -261
- package/lib/execution/match.test.mjs +0 -235
- package/lib/execution/pipeline.test.mjs +0 -392
- package/lib/execution/route.test.mjs +0 -186
- package/lib/execution/surface-policy.test.mjs +0 -162
- package/lib/fs-atomic.test.mjs +0 -72
- package/lib/fs-ownership.test.mjs +0 -158
- package/lib/goals/admission.test.mjs +0 -164
- package/lib/goals/classify.test.mjs +0 -167
- package/lib/goals/collaborate.test.mjs +0 -336
- package/lib/goals/gaps.test.mjs +0 -284
- package/lib/goals/loop.test.mjs +0 -845
- package/lib/hooks/bus.test.mjs +0 -387
- package/lib/identity/persona.test.mjs +0 -142
- package/lib/kpi-sensors.test.mjs +0 -278
- package/lib/kpi.test.mjs +0 -244
- package/lib/learning/config.test.mjs +0 -75
- package/lib/learning/counters.test.mjs +0 -69
- package/lib/learning/curator-consolidate.test.mjs +0 -238
- package/lib/learning/curator.test.mjs +0 -106
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.test.mjs +0 -125
- package/lib/learning/skill-writer.test.mjs +0 -210
- package/lib/mandate/audit.test.mjs +0 -195
- package/lib/mandate/contract.test.mjs +0 -185
- package/lib/mandate/derive.test.mjs +0 -274
- package/lib/mandate/model.test.mjs +0 -164
- package/lib/mandate/refresh.test.mjs +0 -389
- package/lib/mcp/server.test.mjs +0 -426
- package/lib/model-router/auth-profiles.test.mjs +0 -580
- package/lib/model-router/catalog.test.mjs +0 -385
- package/lib/model-router/economics.test.mjs +0 -438
- package/lib/model-router/failover.test.mjs +0 -439
- package/lib/model-router/health.test.mjs +0 -338
- package/lib/model-router/integration-coverage.test.mjs +0 -831
- package/lib/model-router/integration.test.mjs +0 -564
- package/lib/model-router/ledger.test.mjs +0 -415
- package/lib/model-router/llm-task.test.mjs +0 -392
- package/lib/model-router/org-credentials.test.mjs +0 -265
- package/lib/model-router/pricing-refresh.test.mjs +0 -286
- package/lib/model-router/reconcile.test.mjs +0 -316
- package/lib/model-router/repair.test.mjs +0 -180
- package/lib/model-router/spawn.test.mjs +0 -446
- package/lib/model-router/taxonomy.test.mjs +0 -410
- package/lib/model-router.test.mjs +0 -1207
- package/lib/org/activity.test.mjs +0 -134
- package/lib/org/approvals.test.mjs +0 -216
- package/lib/org/awareness.test.mjs +0 -159
- package/lib/org/board-mine-cache.test.mjs +0 -53
- package/lib/org/board.test.mjs +0 -187
- package/lib/org/bootstrap-context.test.mjs +0 -153
- package/lib/org/client.test.mjs +0 -1206
- package/lib/org/cohort-client.test.mjs +0 -126
- package/lib/org/cost-sync.test.mjs +0 -153
- package/lib/org/doctor.test.mjs +0 -346
- package/lib/org/engagement-ledger.test.mjs +0 -112
- package/lib/org/engagement.test.mjs +0 -739
- package/lib/org/handoff.test.mjs +0 -269
- package/lib/org/inbound/directedness.test.mjs +0 -668
- package/lib/org/inbound/facts.test.mjs +0 -471
- package/lib/org/inbound/hydrate.test.mjs +0 -908
- package/lib/org/inbound/index.test.mjs +0 -429
- package/lib/org/inbound/project.test.mjs +0 -287
- package/lib/org/integration-tools.test.mjs +0 -160
- package/lib/org/keys.test.mjs +0 -92
- package/lib/org/knowledge.test.mjs +0 -326
- package/lib/org/leases.test.mjs +0 -235
- package/lib/org/mesh-directives.test.mjs +0 -110
- package/lib/org/mesh-integration.test.mjs +0 -127
- package/lib/org/mesh.test.mjs +0 -400
- package/lib/org/messaging.test.mjs +0 -471
- package/lib/org/param-contract.test.mjs +0 -477
- package/lib/org/policy.test.mjs +0 -237
- package/lib/org/protocol.checksum.test.mjs +0 -90
- package/lib/org/protocol.test.mjs +0 -323
- package/lib/org/push.test.mjs +0 -792
- package/lib/org/registry.test.mjs +0 -100
- package/lib/org/resource-tools.test.mjs +0 -361
- package/lib/org/tool-access.test.mjs +0 -144
- package/lib/org/tool-surface-integration.test.mjs +0 -120
- package/lib/org/tool-surface.test.mjs +0 -1268
- package/lib/org/typing.test.mjs +0 -291
- package/lib/org/ui-parity.test.mjs +0 -560
- package/lib/org/verify.test.mjs +0 -194
- package/lib/org/work-ledger.test.mjs +0 -273
- package/lib/plan/adoption-e2e.test.mjs +0 -366
- package/lib/plan/budget-enforcement.test.mjs +0 -400
- package/lib/plan/compile.test.mjs +0 -382
- package/lib/plan/emit.test.mjs +0 -269
- package/lib/plan/explain.test.mjs +0 -188
- package/lib/prompts/parallelism.test.mjs +0 -177
- package/lib/rag/rag.test.mjs +0 -505
- package/lib/rate-guard.test.mjs +0 -272
- package/lib/reactive-gate.test.mjs +0 -57
- package/lib/render.test.mjs +0 -68
- package/lib/resource-governor.test.mjs +0 -488
- package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
- package/lib/scheduling/jitter.test.mjs +0 -140
- package/lib/secrets/broker.test.mjs +0 -280
- package/lib/secrets/providers.test.mjs +0 -274
- package/lib/security/audit-engine.test.mjs +0 -424
- package/lib/security/coerce-args.test.mjs +0 -281
- package/lib/security/dangerous-tools.test.mjs +0 -68
- package/lib/security/external-content.test.mjs +0 -84
- package/lib/security/redact.test.mjs +0 -441
- package/lib/security/secret-equal.test.mjs +0 -55
- package/lib/session/config.test.mjs +0 -92
- package/lib/session/feed-core.test.mjs +0 -198
- package/lib/session/first-run.test.mjs +0 -121
- package/lib/session/frontdoor.test.mjs +0 -205
- package/lib/session/handoffs.test.mjs +0 -183
- package/lib/session/identity.test.mjs +0 -180
- package/lib/session/inbox-claims.test.mjs +0 -286
- package/lib/session/launch-args.test.mjs +0 -157
- package/lib/session/liveness.test.mjs +0 -100
- package/lib/session/status-summary.test.mjs +0 -118
- package/lib/session-permissions.test.mjs +0 -120
- package/lib/setup/claude-probe.test.mjs +0 -187
- package/lib/setup/completeness.test.mjs +0 -110
- package/lib/setup/context-pack.test.mjs +0 -89
- package/lib/setup/enrich.test.mjs +0 -115
- package/lib/setup/enroll-from-cohort.test.mjs +0 -300
- package/lib/setup/integration.test.mjs +0 -162
- package/lib/setup/io.test.mjs +0 -77
- package/lib/setup/runner.test.mjs +0 -132
- package/lib/setup/sections/identity.test.mjs +0 -234
- package/lib/setup/sections/inventory.test.mjs +0 -198
- package/lib/setup/sections/learning.test.mjs +0 -81
- package/lib/setup/sections/mandate.test.mjs +0 -388
- package/lib/setup/sections/messaging.test.mjs +0 -127
- package/lib/setup/sections/model.test.mjs +0 -240
- package/lib/setup/sections/org.test.mjs +0 -346
- package/lib/setup/sections/orgmail.test.mjs +0 -118
- package/lib/setup/sections/recovery.test.mjs +0 -98
- package/lib/setup/sections/subagents.test.mjs +0 -429
- package/lib/setup/sections/verify.test.mjs +0 -175
- package/lib/setup/sot.test.mjs +0 -81
- package/lib/setup/state.test.mjs +0 -115
- package/lib/singleton.test.mjs +0 -151
- package/lib/subagents/cli.test.mjs +0 -389
- package/lib/subagents/client.test.mjs +0 -309
- package/lib/subagents/gap.test.mjs +0 -234
- package/lib/subagents/lock.test.mjs +0 -248
- package/lib/subagents/manifest.test.mjs +0 -175
- package/lib/subagents/refs.test.mjs +0 -204
- package/lib/subagents/resolve.test.mjs +0 -422
- package/lib/subagents/schema.test.mjs +0 -328
- package/lib/telemetry/alerts.test.mjs +0 -109
- package/lib/telemetry/collect.test.mjs +0 -1274
- package/lib/tool-definitions-integration.test.mjs +0 -83
- package/lib/tool-definitions.test.mjs +0 -437
- package/lib/upgrade/global-refresh.test.mjs +0 -65
- package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
- package/lib/upgrade/post-steps.test.mjs +0 -200
- package/lib/upgrade/verify.test.mjs +0 -164
- package/lib/util/fetch-timeout.test.mjs +0 -202
- package/lib/util/reconnect.test.mjs +0 -369
- package/lib/util/unhandled.test.mjs +0 -216
- package/lib/voice/outbound.test.mjs +0 -69
- package/lib/voice/session-rotation.test.mjs +0 -114
- package/lib/voice/stt.test.mjs +0 -226
- package/lib/voice/voice.test.mjs +0 -990
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
- package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
- package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
- package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
- package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
- package/scripts/ci/check-skill-packs.test.mjs +0 -495
- package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
- package/scripts/ci/check.test.mjs +0 -194
- package/scripts/ci/conformance-org-api.test.mjs +0 -425
- package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
- package/scripts/collective/hook-runner.test.mjs +0 -173
- package/scripts/cost/fleet-digest.test.mjs +0 -207
- package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
- package/scripts/cost/track-claude-usage.test.mjs +0 -148
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
- package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
- package/scripts/daemon/agent-daemon.test.mjs +0 -995
- package/scripts/daemon/assurance-e2e.test.mjs +0 -613
- package/scripts/daemon/assurance.test.mjs +0 -1791
- package/scripts/daemon/board-mirror.test.mjs +0 -165
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
- package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
- package/scripts/daemon/cadence-consumer.test.mjs +0 -776
- package/scripts/daemon/cadence-handlers.test.mjs +0 -837
- package/scripts/daemon/classifier-identity.test.mjs +0 -137
- package/scripts/daemon/classifier.test.mjs +0 -266
- package/scripts/daemon/classify-kind.test.mjs +0 -40
- package/scripts/daemon/context-compiler.test.mjs +0 -406
- package/scripts/daemon/deliver.test.mjs +0 -564
- package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
- package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
- package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
- package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
- package/scripts/daemon/execution-ladder.test.mjs +0 -470
- package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
- package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
- package/scripts/daemon/inbox-deferral.test.mjs +0 -336
- package/scripts/daemon/inbox-wake.test.mjs +0 -199
- package/scripts/daemon/integration.test.mjs +0 -149
- package/scripts/daemon/lib/self-echo.test.mjs +0 -153
- package/scripts/daemon/lib/session-router.test.mjs +0 -554
- package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
- package/scripts/daemon/prompt-builder.test.mjs +0 -556
- package/scripts/daemon/responder-cost.test.mjs +0 -68
- package/scripts/daemon/responder-history.test.mjs +0 -221
- package/scripts/daemon/sdk-version.test.mjs +0 -31
- package/scripts/daemon/session-lock.test.mjs +0 -252
- package/scripts/daemon/session-outcomes.test.mjs +0 -533
- package/scripts/daemon/typing-registry.test.mjs +0 -102
- package/scripts/hooks/pre-send-audit.test.mjs +0 -354
- package/scripts/huddle/huddle-prompt.test.mjs +0 -176
- package/scripts/local-triggers/autoupdate.test.mjs +0 -518
- package/scripts/local-triggers/generate-plists.test.mjs +0 -456
- package/scripts/media-generation/brand-clause.test.mjs +0 -135
- package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
- package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
- package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
- package/scripts/poller/slack-socket-mode.test.mjs +0 -805
- package/scripts/poller-launchd/install.test.mjs +0 -243
- package/scripts/restore-from-backup.test.mjs +0 -181
- package/scripts/session/feed.test.mjs +0 -196
- package/scripts/session/supervisor-sh.test.mjs +0 -218
- package/scripts/session/supervisor.test.mjs +0 -482
- package/scripts/setup/configure-macos.test.mjs +0 -306
- package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
- package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
- package/scripts/setup/generate-capability.test.mjs +0 -134
- package/scripts/setup/init-agent.test.mjs +0 -370
- package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
- package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
- package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/commands/index.mjs — slash commands in a prompt (W4-E2, conformance
|
|
3
|
+
* rows 14 and 34).
|
|
4
|
+
*
|
|
5
|
+
* A prompt (the -p prompt, or a stream-json user message) whose text starts with
|
|
6
|
+
* `/<name>` followed by whitespace or the end is a slash invocation:
|
|
7
|
+
*
|
|
8
|
+
* /review src/loop.mjs name "review", arguments "src/loop.mjs"
|
|
9
|
+
* /maestro:main-session a namespaced name
|
|
10
|
+
* /Users/me/notes.txt NOT an invocation (the name is not followed by
|
|
11
|
+
* whitespace or the end), passed on as written
|
|
12
|
+
*
|
|
13
|
+
* The name resolves, in order, to:
|
|
14
|
+
* 1. a project command <cwd>/.claude/commands/**\/*.md
|
|
15
|
+
* 2. a user command ~/.claude/commands/**\/*.md
|
|
16
|
+
* 3. a skill skills/index.mjs `findSkill` (exact name, plugin namespace
|
|
17
|
+
* alias, the flat installed form `<ns>-<name>`, a unique short name)
|
|
18
|
+
* 4. a plugin command <plugin root>/commands/*.md, named `<plugin>:<name>`
|
|
19
|
+
* A command file in a subdirectory is named by its file name; `<dir>:<name>`
|
|
20
|
+
* (subdirectories joined by ":") is accepted as well. The first name wins within
|
|
21
|
+
* a source; shadowed files are reported.
|
|
22
|
+
*
|
|
23
|
+
* A command file is Markdown with optional frontmatter: `description`,
|
|
24
|
+
* `argument-hint`, `allowed-tools` (recorded, not enforced, as for skills) and
|
|
25
|
+
* `model` (the turn runs on that model's Cohort tier). Its body becomes the
|
|
26
|
+
* turn's instructions: `$ARGUMENTS` is replaced by the argument text and `$1` …
|
|
27
|
+
* `$9` by the arguments split on whitespace (quotes group); a body with neither
|
|
28
|
+
* gets the arguments appended. A skill's body is injected with its base
|
|
29
|
+
* directory, exactly as the Skill tool loads it.
|
|
30
|
+
*
|
|
31
|
+
* An unknown name is not an error: the prompt is passed on as written, with a
|
|
32
|
+
* note saying no command or skill had that name.
|
|
33
|
+
*
|
|
34
|
+
* Everything here is pure except `discoverCommands` and `expandSlashCommand`,
|
|
35
|
+
* which read through an injected fs.
|
|
36
|
+
*
|
|
37
|
+
* Authorship: the injected instruction text was written for Cohort Engine by the
|
|
38
|
+
* Cohort team, 2026-09-14.
|
|
39
|
+
*
|
|
40
|
+
* @module lib/engine/commands/index
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import path from "node:path";
|
|
44
|
+
import { expandPluginRoots, findSkill, parseSkillFile, pluginIdentity } from "../skills/index.mjs";
|
|
45
|
+
|
|
46
|
+
const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]*(?::[A-Za-z0-9][A-Za-z0-9_.-]*)*$/;
|
|
47
|
+
const SLASH_RE = /^\/([A-Za-z0-9][A-Za-z0-9_.-]*(?::[A-Za-z0-9][A-Za-z0-9_.-]*)*)(?:[ \t\r\n]+([\s\S]*))?$/;
|
|
48
|
+
const MAX_COMMAND_DEPTH = 3;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {Object} Command
|
|
52
|
+
* @property {string} name
|
|
53
|
+
* @property {string[]} aliases
|
|
54
|
+
* @property {string} description
|
|
55
|
+
* @property {string|null} argumentHint
|
|
56
|
+
* @property {string[]} allowedTools
|
|
57
|
+
* @property {string|null} model
|
|
58
|
+
* @property {string} file
|
|
59
|
+
* @property {'project'|'user'|'plugin'} source
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/** @param {unknown} v */
|
|
63
|
+
function toolList(v) {
|
|
64
|
+
if (Array.isArray(v)) return v.map(String).map((s) => s.trim()).filter(Boolean);
|
|
65
|
+
if (typeof v === "string") return v.split(/[,\s]+/).map((s) => s.trim()).filter(Boolean);
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Is this text a slash invocation? Pure.
|
|
71
|
+
* @param {string} text
|
|
72
|
+
* @returns {{name:string, args:string}|null}
|
|
73
|
+
*/
|
|
74
|
+
export function parseSlashCommand(text) {
|
|
75
|
+
const m = SLASH_RE.exec(String(text ?? "").replace(/^\s+/, "").replace(/\s+$/, ""));
|
|
76
|
+
return m ? { name: m[1], args: (m[2] ?? "").trim() } : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Split arguments on whitespace; single or double quotes group. Pure.
|
|
81
|
+
* @param {string} args
|
|
82
|
+
*/
|
|
83
|
+
export function splitArguments(args) {
|
|
84
|
+
const out = [];
|
|
85
|
+
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
86
|
+
for (let m = re.exec(args); m; m = re.exec(args)) out.push(m[1] ?? m[2] ?? m[3]);
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A command body with its arguments substituted. Pure.
|
|
92
|
+
* @param {string} body @param {string} args
|
|
93
|
+
*/
|
|
94
|
+
export function substituteArguments(body, args) {
|
|
95
|
+
const positional = splitArguments(args);
|
|
96
|
+
const hasArguments = body.includes("$ARGUMENTS");
|
|
97
|
+
const hasPositional = /\$[1-9](?!\d)/.test(body);
|
|
98
|
+
let out = hasArguments ? body.split("$ARGUMENTS").join(args) : body;
|
|
99
|
+
if (hasPositional) out = out.replace(/\$([1-9])(?!\d)/g, (_, n) => positional[Number(n) - 1] ?? "");
|
|
100
|
+
if (!hasArguments && !hasPositional && args) out = `${out}\n\nArguments: ${args}`;
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {{file:string, text:string, name:string, aliases:string[], source:Command['source']}} p
|
|
106
|
+
* @returns {Command}
|
|
107
|
+
*/
|
|
108
|
+
export function commandFromFile({ file, text, name, aliases, source }) {
|
|
109
|
+
const { frontmatter: fm, body } = parseSkillFile(text);
|
|
110
|
+
let description = typeof fm.description === "string" ? fm.description.trim() : "";
|
|
111
|
+
if (description === "") description = (body.split(/\r?\n/).map((l) => l.replace(/^#+\s*/, "").trim()).find((l) => l !== "") ?? "").slice(0, 200);
|
|
112
|
+
const hint = fm["argument-hint"] ?? fm.argument_hint;
|
|
113
|
+
const model = typeof fm.model === "string" && fm.model.trim() !== "" ? fm.model.trim() : null;
|
|
114
|
+
return { name, aliases, description, argumentHint: typeof hint === "string" ? hint : null, allowedTools: toolList(fm["allowed-tools"] ?? fm.allowed_tools), model, file, source };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @typedef {Object} CommandFs
|
|
119
|
+
* @property {(p:string)=>string} readFile
|
|
120
|
+
* @property {(p:string)=>string[]} readdir
|
|
121
|
+
* @property {(p:string)=>boolean} isFile
|
|
122
|
+
* @property {(p:string)=>boolean} isDir
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {object} p
|
|
127
|
+
* @param {string|null} p.userDir ~/.claude (null: no user commands)
|
|
128
|
+
* @param {string|null} p.cwd null: no project commands (--bare)
|
|
129
|
+
* @param {Array<{dir:string}>} [p.pluginDirs]
|
|
130
|
+
* @param {CommandFs} p.fs
|
|
131
|
+
* @returns {{commands: Command[], errors: string[]}}
|
|
132
|
+
*/
|
|
133
|
+
export function discoverCommands({ userDir, cwd, pluginDirs = [], fs }) {
|
|
134
|
+
/** @type {Command[]} */
|
|
135
|
+
const commands = [];
|
|
136
|
+
const errors = [];
|
|
137
|
+
const names = (/** @type string */ dir) => {
|
|
138
|
+
try {
|
|
139
|
+
return fs.readdir(dir).sort();
|
|
140
|
+
} catch {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
/** @param {string} dir @param {Command['source']} source @param {string|null} namespace @param {string[]} nsAliases */
|
|
145
|
+
const scan = (dir, source, namespace, nsAliases) => {
|
|
146
|
+
/** @type {Map<string, Command>} */
|
|
147
|
+
const seen = new Map();
|
|
148
|
+
/** @param {string} d @param {string[]} parents */
|
|
149
|
+
const walk = (d, parents) => {
|
|
150
|
+
if (!fs.isDir(d)) return;
|
|
151
|
+
for (const entry of names(d)) {
|
|
152
|
+
const full = path.join(d, entry);
|
|
153
|
+
if (entry.startsWith(".")) continue;
|
|
154
|
+
if (fs.isDir(full)) {
|
|
155
|
+
if (namespace === null && parents.length + 1 < MAX_COMMAND_DEPTH) walk(full, [...parents, entry]);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (!entry.endsWith(".md") || !fs.isFile(full)) continue;
|
|
159
|
+
const base = entry.slice(0, -3);
|
|
160
|
+
if (!NAME_RE.test(base)) continue;
|
|
161
|
+
const name = namespace ? `${namespace}:${base}` : base;
|
|
162
|
+
const aliases = namespace ? nsAliases.map((a) => `${a}:${base}`) : parents.length ? [[...parents, base].join(":")] : [];
|
|
163
|
+
if (seen.has(name)) {
|
|
164
|
+
errors.push(`command "${name}" at ${full} is shadowed by ${seen.get(name)?.file}`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
let text;
|
|
168
|
+
try {
|
|
169
|
+
text = fs.readFile(full);
|
|
170
|
+
} catch (e) {
|
|
171
|
+
errors.push(`command ${full}: cannot read (${e instanceof Error ? e.message : e})`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const c = commandFromFile({ file: full, text, name, aliases: aliases.filter((a) => NAME_RE.test(a)), source });
|
|
175
|
+
seen.set(name, c);
|
|
176
|
+
commands.push(c);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
walk(dir, []);
|
|
180
|
+
};
|
|
181
|
+
if (cwd) scan(path.join(cwd, ".claude", "commands"), "project", null, []);
|
|
182
|
+
if (userDir) scan(path.join(userDir, "commands"), "user", null, []);
|
|
183
|
+
for (const root of expandPluginRoots(pluginDirs, fs)) {
|
|
184
|
+
if (!fs.isDir(root)) continue; // skills discovery reports a missing plugin root
|
|
185
|
+
const identity = pluginIdentity(root, fs);
|
|
186
|
+
scan(path.join(root, "commands"), "plugin", identity.name, identity.aliases);
|
|
187
|
+
}
|
|
188
|
+
return { commands, errors };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Find a command by name among one source's commands. Pure.
|
|
193
|
+
* @param {Command[]} commands @param {string} name
|
|
194
|
+
*/
|
|
195
|
+
function findCommand(commands, name) {
|
|
196
|
+
const exact = commands.find((c) => c.name === name) ?? commands.find((c) => c.aliases.includes(name));
|
|
197
|
+
if (exact) return exact;
|
|
198
|
+
const colon = name.indexOf(":");
|
|
199
|
+
if (colon > 0 && name.indexOf(":", colon + 1) === -1) return commands.find((c) => c.name === `${name.slice(0, colon)}-${name.slice(colon + 1)}`) ?? null;
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* What a prompt's slash invocation names. Pure.
|
|
205
|
+
* @param {{text:string, commands:Command[], skills:import('../skills/index.mjs').Skill[]}} p
|
|
206
|
+
* @returns {{kind:'none'} | {kind:'command', name:string, args:string, command:Command} | {kind:'skill', name:string, args:string, skill:import('../skills/index.mjs').Skill} | {kind:'unknown', name:string, args:string, reason:string|null}}
|
|
207
|
+
*/
|
|
208
|
+
export function resolveSlashCommand({ text, commands, skills }) {
|
|
209
|
+
const parsed = parseSlashCommand(text);
|
|
210
|
+
if (!parsed) return { kind: "none" };
|
|
211
|
+
const { name, args } = parsed;
|
|
212
|
+
for (const source of /** @type {const} */ (["project", "user"])) {
|
|
213
|
+
const c = findCommand(commands.filter((x) => x.source === source), name);
|
|
214
|
+
if (c) return { kind: "command", name, args, command: c };
|
|
215
|
+
}
|
|
216
|
+
const s = findSkill(skills, name);
|
|
217
|
+
if (s.ok) return { kind: "skill", name, args, skill: s.skill };
|
|
218
|
+
const p = findCommand(commands.filter((x) => x.source === "plugin"), name);
|
|
219
|
+
if (p) return { kind: "command", name, args, command: p };
|
|
220
|
+
// A short name that several plugin skills share is ambiguous, not unknown.
|
|
221
|
+
return { kind: "unknown", name, args, reason: /matches several skills/.test(s.error) ? s.error : null };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The note an unknown slash command's prompt carries. Pure.
|
|
226
|
+
* @param {string} name @param {string|null} [reason]
|
|
227
|
+
*/
|
|
228
|
+
export function unknownCommandNote(name, reason = null) {
|
|
229
|
+
return `[Note: "/${name}" is not a command or skill available in this session${reason ? ` (${reason})` : ""}; the message above is passed on as written.]`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The text a resolved invocation puts in the turn, read through `fs`.
|
|
234
|
+
* @param {Exclude<ReturnType<typeof resolveSlashCommand>, {kind:'none'}>} resolved
|
|
235
|
+
* @param {{readFile:(p:string)=>string}} fs
|
|
236
|
+
* @param {string} original the prompt as typed
|
|
237
|
+
* @returns {{ok:true, text:string, model:string|null, allowedTools:string[], invoked:{kind:string, name:string, file:string|null}|null} | {ok:false, error:string}}
|
|
238
|
+
*/
|
|
239
|
+
export function expandSlashCommand(resolved, fs, original) {
|
|
240
|
+
if (resolved.kind === "unknown") {
|
|
241
|
+
return { ok: true, text: `${original}\n\n${unknownCommandNote(resolved.name, resolved.reason)}`, model: null, allowedTools: [], invoked: null };
|
|
242
|
+
}
|
|
243
|
+
const file = resolved.kind === "command" ? resolved.command.file : resolved.skill.file;
|
|
244
|
+
let raw;
|
|
245
|
+
try {
|
|
246
|
+
raw = fs.readFile(file);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
return { ok: false, error: `/${resolved.name}: ${file} could not be read (${e instanceof Error ? e.message : e})` };
|
|
249
|
+
}
|
|
250
|
+
const body = substituteArguments(parseSkillFile(raw).body.trim(), resolved.args);
|
|
251
|
+
if (resolved.kind === "command") {
|
|
252
|
+
const c = resolved.command;
|
|
253
|
+
return {
|
|
254
|
+
ok: true,
|
|
255
|
+
text: `The user ran the /${c.name} command${resolved.args ? ` with the arguments: ${resolved.args}` : ""}. Carry out its instructions, which follow.\n\n${body}`,
|
|
256
|
+
model: c.model,
|
|
257
|
+
allowedTools: c.allowedTools,
|
|
258
|
+
invoked: { kind: "command", name: c.name, file: c.file },
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const s = resolved.skill;
|
|
262
|
+
return {
|
|
263
|
+
ok: true,
|
|
264
|
+
text: `The user invoked the "${s.name}" skill${resolved.args ? ` with the arguments: ${resolved.args}` : ""}. Its base directory is ${s.baseDir}; resolve relative paths in these instructions against it. Follow its instructions below.\n\n${body}`,
|
|
265
|
+
model: null,
|
|
266
|
+
allowedTools: s.allowedTools,
|
|
267
|
+
invoked: { kind: "skill", name: s.name, file: s.file },
|
|
268
|
+
};
|
|
269
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/context/budget.mjs — how big a request is, and when to compact (pure).
|
|
3
|
+
*
|
|
4
|
+
* Context windows are DATA: `CONTEXT_WINDOWS` maps each Cohort tier to the
|
|
5
|
+
* window the engine plans against. A settings file (`cohort.contextWindows`)
|
|
6
|
+
* or `COHORT_ENGINE_CONTEXT_WINDOW` overrides it, because the slot map behind a
|
|
7
|
+
* tier is decided by the parity eval and can change without an engine release.
|
|
8
|
+
*
|
|
9
|
+
* Estimation is a characters-per-token heuristic, CALIBRATED by the provider:
|
|
10
|
+
* after every model call the loop reports the input tokens the gateway counted
|
|
11
|
+
* for a request of a known size (`calibrate`). The projection for the next
|
|
12
|
+
* request is then the provider's own count plus the growth since, converted at
|
|
13
|
+
* the observed ratio (`projectTokens`). Before the first call the default
|
|
14
|
+
* ratio (4 characters per token) is used. Images are counted at a flat
|
|
15
|
+
* estimate each; their bytes say little about their token cost.
|
|
16
|
+
*
|
|
17
|
+
* Compaction fires when the projected input reaches `threshold` of the window
|
|
18
|
+
* (default 85%). Everything here is a function of its inputs: no clock, no
|
|
19
|
+
* environment read — `resolveContextConfig` takes the environment as a
|
|
20
|
+
* parameter.
|
|
21
|
+
*
|
|
22
|
+
* @module lib/engine/context/budget
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** The window, in tokens, the engine plans against per tier. */
|
|
26
|
+
export const CONTEXT_WINDOWS = Object.freeze({
|
|
27
|
+
"cohort-agentic": 1_000_000,
|
|
28
|
+
"cohort-reason": 1_000_000,
|
|
29
|
+
"cohort-fast": 128_000,
|
|
30
|
+
"cohort-extract": 128_000,
|
|
31
|
+
"cohort-vision": 128_000,
|
|
32
|
+
});
|
|
33
|
+
/** A tier the table does not know gets the smallest window any tier has. */
|
|
34
|
+
export const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
35
|
+
export const DEFAULT_COMPACT_THRESHOLD = 0.85;
|
|
36
|
+
export const DEFAULT_CHARS_PER_TOKEN = 4;
|
|
37
|
+
/** Tool results kept verbatim by stage-1 compaction. */
|
|
38
|
+
export const DEFAULT_KEEP_TOOL_RESULTS = 6;
|
|
39
|
+
/** Messages kept verbatim after a stage-2 summary (rounded back to an assistant turn). */
|
|
40
|
+
export const DEFAULT_KEEP_RECENT_MESSAGES = 6;
|
|
41
|
+
/** Image-carrying tool results whose images stay in the history (images.mjs). */
|
|
42
|
+
export const DEFAULT_KEEP_IMAGE_RESULTS = 3;
|
|
43
|
+
/** Tokens one image is assumed to cost. */
|
|
44
|
+
export const IMAGE_TOKEN_ESTIMATE = 1_600;
|
|
45
|
+
const MIN_RATIO = 1;
|
|
46
|
+
const MAX_RATIO = 10;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @typedef {Object} ContextConfig
|
|
50
|
+
* @property {number} contextWindow
|
|
51
|
+
* @property {number} threshold 0 < threshold ≤ 1
|
|
52
|
+
* @property {boolean} autoCompact
|
|
53
|
+
* @property {number} keepToolResults
|
|
54
|
+
* @property {number} keepRecentMessages
|
|
55
|
+
* @property {number} [keepImageResults] image-carrying tool results whose images stay in the history
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} tier
|
|
60
|
+
* @param {Record<string, unknown>} [overrides] settings `cohort.contextWindows`
|
|
61
|
+
*/
|
|
62
|
+
export function contextWindowFor(tier, overrides = {}) {
|
|
63
|
+
const o = overrides?.[tier];
|
|
64
|
+
if (typeof o === "number" && Number.isInteger(o) && o > 0) return o;
|
|
65
|
+
return /** @type {Record<string,number>} */ (CONTEXT_WINDOWS)[tier] ?? DEFAULT_CONTEXT_WINDOW;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse a threshold: a fraction (0.85) or a percentage (85).
|
|
70
|
+
* @param {unknown} v @returns {number|null}
|
|
71
|
+
*/
|
|
72
|
+
export function parseThreshold(v) {
|
|
73
|
+
const n = typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" ? Number(v.trim().replace(/%$/, "")) : NaN;
|
|
74
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
75
|
+
const f = n > 1 ? n / 100 : n;
|
|
76
|
+
return f > 0 && f <= 1 ? f : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The run's context settings. Precedence: environment > settings > defaults.
|
|
81
|
+
*
|
|
82
|
+
* settings cohort.contextWindows {tier: tokens}
|
|
83
|
+
* cohort.compaction {threshold, auto, keepToolResults, keepRecentMessages, keepImageResults}
|
|
84
|
+
* env COHORT_ENGINE_CONTEXT_WINDOW, COHORT_ENGINE_COMPACT_THRESHOLD,
|
|
85
|
+
* COHORT_ENGINE_AUTO_COMPACT (0/false/off disables automatic compaction)
|
|
86
|
+
*
|
|
87
|
+
* @param {{tier:string, settings?:any, env?:Record<string,string|undefined>}} p
|
|
88
|
+
* @returns {{config: ContextConfig, warnings: string[]}}
|
|
89
|
+
*/
|
|
90
|
+
export function resolveContextConfig({ tier, settings = {}, env = {} }) {
|
|
91
|
+
const warnings = [];
|
|
92
|
+
const s = settings && typeof settings === "object" ? settings : {};
|
|
93
|
+
const comp = s.compaction && typeof s.compaction === "object" ? s.compaction : {};
|
|
94
|
+
let contextWindow = contextWindowFor(tier, s.contextWindows);
|
|
95
|
+
if (env.COHORT_ENGINE_CONTEXT_WINDOW !== undefined && env.COHORT_ENGINE_CONTEXT_WINDOW !== "") {
|
|
96
|
+
const n = Number(env.COHORT_ENGINE_CONTEXT_WINDOW);
|
|
97
|
+
if (Number.isInteger(n) && n > 0) contextWindow = n;
|
|
98
|
+
else warnings.push(`COHORT_ENGINE_CONTEXT_WINDOW "${env.COHORT_ENGINE_CONTEXT_WINDOW}" is not a positive integer (ignored)`);
|
|
99
|
+
}
|
|
100
|
+
let threshold = DEFAULT_COMPACT_THRESHOLD;
|
|
101
|
+
if (comp.threshold !== undefined) {
|
|
102
|
+
const t = parseThreshold(comp.threshold);
|
|
103
|
+
if (t === null) warnings.push(`settings cohort.compaction.threshold ${JSON.stringify(comp.threshold)} is not a fraction or percentage (ignored)`);
|
|
104
|
+
else threshold = t;
|
|
105
|
+
}
|
|
106
|
+
if (env.COHORT_ENGINE_COMPACT_THRESHOLD !== undefined && env.COHORT_ENGINE_COMPACT_THRESHOLD !== "") {
|
|
107
|
+
const t = parseThreshold(env.COHORT_ENGINE_COMPACT_THRESHOLD);
|
|
108
|
+
if (t === null) warnings.push(`COHORT_ENGINE_COMPACT_THRESHOLD "${env.COHORT_ENGINE_COMPACT_THRESHOLD}" is not a fraction or percentage (ignored)`);
|
|
109
|
+
else threshold = t;
|
|
110
|
+
}
|
|
111
|
+
let autoCompact = comp.auto !== false;
|
|
112
|
+
const envAuto = String(env.COHORT_ENGINE_AUTO_COMPACT ?? "").trim().toLowerCase();
|
|
113
|
+
if (["0", "false", "off", "no"].includes(envAuto)) autoCompact = false;
|
|
114
|
+
else if (["1", "true", "on", "yes"].includes(envAuto)) autoCompact = true;
|
|
115
|
+
const posInt = (/** @type unknown */ v, /** @type number */ d) => (typeof v === "number" && Number.isInteger(v) && v >= 0 ? v : d);
|
|
116
|
+
return {
|
|
117
|
+
config: {
|
|
118
|
+
contextWindow,
|
|
119
|
+
threshold,
|
|
120
|
+
autoCompact,
|
|
121
|
+
keepToolResults: posInt(comp.keepToolResults, DEFAULT_KEEP_TOOL_RESULTS),
|
|
122
|
+
keepRecentMessages: Math.max(2, posInt(comp.keepRecentMessages, DEFAULT_KEEP_RECENT_MESSAGES)),
|
|
123
|
+
keepImageResults: posInt(comp.keepImageResults, DEFAULT_KEEP_IMAGE_RESULTS),
|
|
124
|
+
},
|
|
125
|
+
warnings,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The size of one request, in characters and images.
|
|
131
|
+
* @param {{system?:string, messages?:any[], tools?:any[]}} req
|
|
132
|
+
* @returns {{chars:number, images:number}}
|
|
133
|
+
*/
|
|
134
|
+
export function measureRequest({ system = "", messages = [], tools = [] }) {
|
|
135
|
+
let chars = String(system ?? "").length;
|
|
136
|
+
let images = 0;
|
|
137
|
+
if (tools.length > 0) chars += JSON.stringify(tools).length;
|
|
138
|
+
for (const m of messages) {
|
|
139
|
+
if (!m || !Array.isArray(m.content)) continue;
|
|
140
|
+
for (const b of m.content) {
|
|
141
|
+
if (!b || typeof b !== "object") continue;
|
|
142
|
+
switch (b.type) {
|
|
143
|
+
case "text":
|
|
144
|
+
chars += String(b.text ?? "").length;
|
|
145
|
+
break;
|
|
146
|
+
case "tool_use":
|
|
147
|
+
chars += String(b.name ?? "").length + (b.rawInput ? String(b.rawInput).length : JSON.stringify(b.input ?? {}).length);
|
|
148
|
+
break;
|
|
149
|
+
case "tool_result":
|
|
150
|
+
chars += String(b.content ?? "").length;
|
|
151
|
+
if (Array.isArray(b.images)) images += b.images.length;
|
|
152
|
+
break;
|
|
153
|
+
case "thinking":
|
|
154
|
+
chars += String(b.thinking ?? "").length;
|
|
155
|
+
break;
|
|
156
|
+
default:
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { chars, images };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @typedef {{charsPerToken:number, anchorChars:number|null, anchorImages:number, anchorTokens:number|null}} Calibration
|
|
166
|
+
*/
|
|
167
|
+
|
|
168
|
+
/** @returns {Calibration} */
|
|
169
|
+
export function initialCalibration(charsPerToken = DEFAULT_CHARS_PER_TOKEN) {
|
|
170
|
+
return { charsPerToken, anchorChars: null, anchorImages: 0, anchorTokens: null };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Input tokens a normalised usage record says the provider counted.
|
|
175
|
+
* @param {import('../wire/usage.mjs').NormalisedUsage|null|undefined} u
|
|
176
|
+
*/
|
|
177
|
+
export function reportedInputTokens(u) {
|
|
178
|
+
if (!u) return 0;
|
|
179
|
+
return (u.inputUncached || 0) + (u.cacheRead || 0) + (u.cacheWrite5m || 0) + (u.cacheWrite1h || 0);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Fold one provider measurement into the calibration. A request with no
|
|
184
|
+
* reported input (usage missing or estimated) leaves it unchanged.
|
|
185
|
+
* @param {Calibration} cal
|
|
186
|
+
* @param {{chars:number, images:number, inputTokens:number}} observed
|
|
187
|
+
* @returns {Calibration}
|
|
188
|
+
*/
|
|
189
|
+
export function calibrate(cal, { chars, images, inputTokens }) {
|
|
190
|
+
if (!(inputTokens > 0) || !(chars > 0)) return cal;
|
|
191
|
+
const textTokens = inputTokens - images * IMAGE_TOKEN_ESTIMATE;
|
|
192
|
+
const ratio = textTokens > 0 ? Math.min(MAX_RATIO, Math.max(MIN_RATIO, chars / textTokens)) : cal.charsPerToken;
|
|
193
|
+
return { charsPerToken: ratio, anchorChars: chars, anchorImages: images, anchorTokens: inputTokens };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Projected input tokens for a request of this size.
|
|
198
|
+
* @param {Calibration} cal @param {{chars:number, images:number}} size
|
|
199
|
+
*/
|
|
200
|
+
export function projectTokens(cal, { chars, images }) {
|
|
201
|
+
if (cal.anchorTokens !== null && cal.anchorChars !== null && chars >= cal.anchorChars) {
|
|
202
|
+
return cal.anchorTokens + Math.ceil((chars - cal.anchorChars) / cal.charsPerToken) + Math.max(0, images - cal.anchorImages) * IMAGE_TOKEN_ESTIMATE;
|
|
203
|
+
}
|
|
204
|
+
return Math.ceil(chars / cal.charsPerToken) + images * IMAGE_TOKEN_ESTIMATE;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Characters that fit in `tokens` at the calibrated ratio. @param {Calibration} cal @param {number} tokens */
|
|
208
|
+
export function charsForTokens(cal, tokens) {
|
|
209
|
+
return Math.floor(tokens * cal.charsPerToken);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @param {{projected:number, contextWindow:number, threshold:number}} p
|
|
214
|
+
* @returns {{over:boolean, limit:number}}
|
|
215
|
+
*/
|
|
216
|
+
export function compactionLimit({ projected, contextWindow, threshold }) {
|
|
217
|
+
const limit = Math.floor(contextWindow * threshold);
|
|
218
|
+
return { over: projected >= limit, limit };
|
|
219
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/context/cache.mjs — prompt-cache discipline (pure).
|
|
3
|
+
*
|
|
4
|
+
* A supplier caches a request's PREFIX. Every cached token is read at a
|
|
5
|
+
* fraction of the input price, so what the engine controls is that the prefix
|
|
6
|
+
* of turn N+1 is byte-identical to the whole of turn N, and that two runs of
|
|
7
|
+
* the same seat in the same project start from the same bytes:
|
|
8
|
+
*
|
|
9
|
+
* · the system prompt is built once per run; per-run values (date,
|
|
10
|
+
* working directory) sit at its end, and nothing is added to it mid-run —
|
|
11
|
+
* hook context, reminders, lazily found instruction files and compaction
|
|
12
|
+
* summaries are appended as later MESSAGES;
|
|
13
|
+
* · tool definitions are ordered deterministically: built-ins in registry
|
|
14
|
+
* order, then engine tools, then MCP tools sorted by name, and every
|
|
15
|
+
* definition is serialised with its keys sorted (`stableToolDefinitions`),
|
|
16
|
+
* so a server that lists its tools or schema keys in a different order on
|
|
17
|
+
* the next run does not change the bytes;
|
|
18
|
+
* · the tool list only ever GROWS at its tail (deferred tools loaded by
|
|
19
|
+
* ToolSearch are appended in load order), so a load rewrites the cache from
|
|
20
|
+
* the tool list on once, after which the new prefix is stable again.
|
|
21
|
+
*
|
|
22
|
+
* Anthropic breakpoints (`planAnthropicBreakpoints`, at most four): the tail
|
|
23
|
+
* of the tool list, the last system block, the last user message of the
|
|
24
|
+
* PREVIOUS turn (it read or wrote the cache last time, so this turn reads it
|
|
25
|
+
* even when many new blocks follow it), and the last block of the final
|
|
26
|
+
* message (which writes the prefix the next turn will read). Placement is a
|
|
27
|
+
* function of the history's shape only. After compaction the history is
|
|
28
|
+
* shorter, and the same function places the two message breakpoints on the new
|
|
29
|
+
* history — the tool and system breakpoints, and their bytes, are unchanged.
|
|
30
|
+
*
|
|
31
|
+
* OpenAI wire: the supplier caches automatically by prefix. `promptCacheKey`
|
|
32
|
+
* derives the routing key (`prompt_cache_key`) sent when the gateway
|
|
33
|
+
* advertises support: one key per project and tier, so sessions of one seat
|
|
34
|
+
* on one project share cache routing.
|
|
35
|
+
*
|
|
36
|
+
* @module lib/engine/context/cache
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { createHash } from "node:crypto";
|
|
40
|
+
|
|
41
|
+
export const MAX_CACHE_BREAKPOINTS = 4;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {{toolCount:number, hasSystem:boolean, messages:Array<{role:string, content:any[]}>}} shape
|
|
45
|
+
* @returns {{tool:number|null, system:boolean, messages:number[]}} message indices, ascending
|
|
46
|
+
*/
|
|
47
|
+
export function planAnthropicBreakpoints({ toolCount, hasSystem, messages }) {
|
|
48
|
+
const tool = toolCount > 0 ? toolCount - 1 : null;
|
|
49
|
+
const picks = new Set();
|
|
50
|
+
const last = messages.length - 1;
|
|
51
|
+
if (last >= 0) {
|
|
52
|
+
picks.add(last);
|
|
53
|
+
// The previous turn's final message: the last user message that precedes
|
|
54
|
+
// the last assistant message.
|
|
55
|
+
let a = last;
|
|
56
|
+
while (a >= 0 && messages[a].role !== "assistant") a--;
|
|
57
|
+
for (let i = a - 1; i >= 0; i--) {
|
|
58
|
+
if (messages[i].role === "user") {
|
|
59
|
+
picks.add(i);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const budget = MAX_CACHE_BREAKPOINTS - (tool !== null ? 1 : 0) - (hasSystem ? 1 : 0);
|
|
65
|
+
const chosen = [...picks].sort((x, y) => y - x).slice(0, Math.max(0, budget)).sort((x, y) => x - y);
|
|
66
|
+
return { tool, system: hasSystem, messages: chosen };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The index of the block in a message that may carry `cache_control`: the last
|
|
71
|
+
* block that is not a thinking block (thinking blocks cannot be marked).
|
|
72
|
+
* @param {{content:any[]}} message @returns {number}
|
|
73
|
+
*/
|
|
74
|
+
export function markableBlockIndex(message) {
|
|
75
|
+
for (let i = message.content.length - 1; i >= 0; i--) if (message.content[i]?.type !== "thinking") return i;
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A deep copy with object keys sorted, so equal values serialise to equal bytes.
|
|
81
|
+
* @param {unknown} v @returns {any}
|
|
82
|
+
*/
|
|
83
|
+
export function canonicalJson(v) {
|
|
84
|
+
if (Array.isArray(v)) return v.map(canonicalJson);
|
|
85
|
+
if (!v || typeof v !== "object") return v;
|
|
86
|
+
/** @type {Record<string, unknown>} */
|
|
87
|
+
const out = {};
|
|
88
|
+
for (const k of Object.keys(v).sort()) out[k] = canonicalJson(/** @type any */ (v)[k]);
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Deterministic order: non-MCP tools as given, then MCP tools sorted by name.
|
|
94
|
+
* @template {{name:string, mcp?:unknown}} T
|
|
95
|
+
* @param {T[]} tools @returns {T[]}
|
|
96
|
+
*/
|
|
97
|
+
export function stableToolOrder(tools) {
|
|
98
|
+
const plain = tools.filter((t) => !t.mcp);
|
|
99
|
+
const mcp = tools.filter((t) => t.mcp).sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
100
|
+
return [...plain, ...mcp];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Wire-facing definitions with canonical schemas.
|
|
105
|
+
* @param {Array<{name:string, description:string, input_schema:object}>} tools
|
|
106
|
+
*/
|
|
107
|
+
export function stableToolDefinitions(tools) {
|
|
108
|
+
return tools.map((t) => ({ name: t.name, description: t.description, input_schema: canonicalJson(t.input_schema) }));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The OpenAI-wire prompt-cache routing key for a project and tier.
|
|
113
|
+
* @param {{cwd:string, model:string}} p
|
|
114
|
+
*/
|
|
115
|
+
export function promptCacheKey({ cwd, model }) {
|
|
116
|
+
return `cohort-${createHash("sha256").update(`${cwd}\u0000${model}`).digest("hex").slice(0, 32)}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Count `cache_control` markers anywhere in a request body (a test and audit aid).
|
|
121
|
+
* @param {unknown} body
|
|
122
|
+
*/
|
|
123
|
+
export function countCacheBreakpoints(body) {
|
|
124
|
+
return (JSON.stringify(body).match(/"cache_control"/g) || []).length;
|
|
125
|
+
}
|